avoigt1121
Claude Opus 5.5
fix(scripts): check_live_surface lists gradio's /_ endpoints instead of comparing them
b598cb3 Download scripts/check_live_surface.py from anne-voigt/pdac-knowledge-agent-dev: direct link, hf CLI and curl.
- Browser
- Download file 6.79 kB
-
https://huggingface.co/spaces/anne-voigt/pdac-knowledge-agent-dev/resolve/main/scripts/check_live_surface.py
- Command line
-
hf download hf://spaces/anne-voigt/pdac-knowledge-agent-dev/scripts/check_live_surface.py
-
curl -L -o check_live_surface.py https://huggingface.co/spaces/anne-voigt/pdac-knowledge-agent-dev/resolve/main/scripts/check_live_surface.py
6.79 kB
| #!/usr/bin/env python3 | |
| """Check a deployed knowledge Space: the gate fails closed, the surface matches the descriptor. | |
| C9 of the Phase 1 plan, run against the live Space rather than the code. ``stage: RUNNING`` only | |
| says the container came up; this signs in the way the orchestrator does and asks real questions. | |
| Two tokens, both the caller's HF token, as the orchestrator sends them: | |
| * ``token=`` β Space *access*. It gets a caller past HF's own wall on a private Space; a public | |
| Space ignores it. The app never reads it. | |
| * ``headers["x-orchestrator-token"]`` β the machine *identity* the allow-list checks. HF's edge | |
| strips ``x-hf-*``, so only this custom header reaches the gate. | |
| Checks, in order (each prints one line; payloads are never dumped): | |
| 1. no identity header β ``denied`` / ``no_token_header`` (with ``--anonymous``: no token at all, | |
| for a public Space, which is what a stranger sends) | |
| 2. a token HF cannot resolve β ``denied`` / ``token_unresolved`` | |
| 3. ``view_api()`` named endpoints and their params == ``deploy/orchestrator_registration.yaml`` | |
| 4. the caller's own identity β ``ok`` on ``annotate_genes`` for ELAVL1 | |
| 5. with ``--lookups``: one real lookup per endpoint β ``ok`` (keyless public sources, no model call) | |
| Exit codes: 0 all passed, 1 a check failed, 2 could not reach the Space, 3 no token. | |
| python scripts/check_live_surface.py --space anne-voigt/pdac-knowledge-agent-dev --lookups | |
| python scripts/check_live_surface.py --space anne-voigt/pdac-knowledge-agent --anonymous | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import yaml | |
| ROOT = Path(__file__).resolve().parent.parent | |
| DESCRIPTOR = ROOT / "deploy" / "orchestrator_registration.yaml" | |
| DEV_SPACE = "anne-voigt/pdac-knowledge-agent-dev" | |
| # Shaped like an HF token so the gate asks HF about it, and HF says no. | |
| _UNRESOLVABLE_TOKEN = "hf_" + "0" * 34 | |
| LOOKUPS = { | |
| "drugs_for_targets": {"genes": "KRAS"}, | |
| "trials_for_genes": {"genes": "CD40", "condition": "pancreatic cancer", "status": "RECRUITING"}, | |
| "annotate_genes": {"genes": "ELAVL1"}, | |
| "pathways_for_genes": {"genes": "ISG15"}, | |
| "targets_for_disease": {"disease": "MONDO_0005184", "size": "5"}, | |
| } | |
| def _token() -> str | None: | |
| token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") | |
| if token: | |
| return token | |
| try: | |
| from huggingface_hub import get_token | |
| return get_token() | |
| except Exception: # noqa: BLE001 β an old hub, or no cache; the caller reports it | |
| return None | |
| def _call(client, endpoint: str, **kwargs) -> dict: | |
| return json.loads(client.predict(**kwargs, api_name=f"/{endpoint}")) | |
| def _describe(payload: dict) -> str: | |
| """The cause in a few named fields, never the rows.""" | |
| if payload.get("status") == "ok": | |
| return ( | |
| f"status='ok' rows={len(payload.get('rows', []))} " | |
| f"unresolved={len(payload.get('unresolved', []))} " | |
| f"source_errors={[e.get('reason') for e in payload.get('source_errors', [])]}" | |
| ) | |
| keys = ("status", "machine_auth", "reason", "message") | |
| return " ".join(f"{k}={payload[k]!r}" for k in keys if k in payload)[:300] | |
| def _expect_denied(client, why: str, label: str) -> bool: | |
| payload = _call(client, "annotate_genes", genes="ELAVL1") | |
| passed = payload.get("status") == "denied" and payload.get("machine_auth") == why | |
| print(f"{'PASS' if passed else 'FAIL'} {label}: {_describe(payload)}") | |
| return passed | |
| def _surface_matches(client) -> bool: | |
| descriptor = yaml.safe_load(DESCRIPTOR.read_text()) | |
| expected = {t["api_name"]: list(t["params"]) for t in descriptor["tools"]} | |
| named = client.view_api(return_format="dict", print_info=False)["named_endpoints"] | |
| # Gradio's own endpoints start with "/_" (hf_oauth adds /_check_login_status); listed, not compared. | |
| internal = sorted(name for name in named if name.startswith("/_")) | |
| live = { | |
| name: [p.get("parameter_name") or p.get("label") for p in spec.get("parameters", [])] | |
| for name, spec in named.items() | |
| if name not in internal | |
| } | |
| passed = live == expected | |
| print( | |
| f"{'PASS' if passed else 'FAIL'} view_api == descriptor: {sorted(live)}" | |
| f" (gradio internals: {internal})" | |
| ) | |
| if not passed: | |
| for name in sorted(set(live) | set(expected)): | |
| if live.get(name) != expected.get(name): | |
| print(f" {name}: live={live.get(name)} descriptor={expected.get(name)}") | |
| return passed | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) | |
| ap.add_argument("--space", default=DEV_SPACE, help=f"Space id (default {DEV_SPACE})") | |
| ap.add_argument( | |
| "--anonymous", action="store_true", help="check 1 sends no token at all (public Space)" | |
| ) | |
| ap.add_argument("--lookups", action="store_true", help="one real lookup per endpoint") | |
| args = ap.parse_args() | |
| token = _token() | |
| if not token: | |
| print("check: no HF token (set HF_TOKEN or `hf auth login`)", file=sys.stderr) | |
| return 3 | |
| from gradio_client import Client | |
| try: | |
| no_identity = Client(args.space, token=None if args.anonymous else token, verbose=False) | |
| unresolvable = Client( | |
| args.space, | |
| token=token, | |
| headers={"x-orchestrator-token": _UNRESOLVABLE_TOKEN}, | |
| verbose=False, | |
| ) | |
| caller = Client( | |
| args.space, token=token, headers={"x-orchestrator-token": token}, verbose=False | |
| ) | |
| except Exception as exc: # noqa: BLE001 β any failure here means "could not reach it" | |
| print(f"check: could not open {args.space}: {type(exc).__name__}: {exc}", file=sys.stderr) | |
| return 2 | |
| print(f"check: {args.space}") | |
| results = [ | |
| _expect_denied( | |
| no_identity, | |
| "no_token_header", | |
| "no token at all" if args.anonymous else "no identity header", | |
| ), | |
| _expect_denied(unresolvable, "token_unresolved", "unresolvable token"), | |
| _surface_matches(caller), | |
| ] | |
| authorized = _call(caller, "annotate_genes", genes="ELAVL1") | |
| results.append(authorized.get("status") == "ok") | |
| print(f"{'PASS' if results[-1] else 'FAIL'} own identity: {_describe(authorized)}") | |
| if args.lookups: | |
| for endpoint, kwargs in LOOKUPS.items(): | |
| payload = _call(caller, endpoint, **kwargs) | |
| results.append(payload.get("status") == "ok" and payload.get("kind") == "annotation") | |
| print(f"{'PASS' if results[-1] else 'FAIL'} {endpoint}: {_describe(payload)}") | |
| return 0 if all(results) else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |