File size: 6,791 Bytes
6e55d2d b598cb3 6e55d2d b598cb3 6e55d2d b598cb3 6e55d2d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | #!/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())
|