Files
turnstone/tests/test_rerank_calibrate.py
T
Patrick Buckley 33ace975d2 feat(models): default-deny governance and admin UI for per-alias backend auth
Follow-up to the per-alias Entra OBO/app-identity backend auth: the
console write path now applies default-deny field classification, the
admin shelf gains full backend-auth support, and the session/registry
rebind machinery is hardened for config changes landing under live
sessions.

Console write gate:
- Default-deny classification: any non-neutral change to a row that is
  or becomes dynamic requires admin.mcp plus validation; the provably
  auth-neutral columns are enumerated (MODEL_AUTH_NEUTRAL_FIELDS) and a
  live-schema classification test forces every future column to be
  classified. The derivation is a pure function (_derive_auth_gate)
  with unit-pinned exclusivity invariants.
- Two-tier validation mirroring the MCP oauth_obo validator: the row
  tier (audience allow-list) runs on every gated write; the posture
  tier (OIDC configured, token store present) runs on pair changes and
  on enable-arming.
- Pure-disable carve-out: disabling a dynamic row is de-escalation and
  is never blocked — admin.models suffices and validation is skipped,
  including for rows with corrupt or skewed stored values.
- Capabilities are compared canonically (key order, integral floats),
  the audience compare normalizes both sides, and staging an audience
  on a static row is refused on both write twins.
- Calibrate writes the capabilities column under an enforced
  confinement invariant with a compare-and-swap persist.

Admin shelf:
- Backend-auth section with a per-open constraints fetch
  (GET /model-definitions/auth-constraints: audience allow-list, grant
  profile, dynamic modes), datalist audience suggestions,
  server-defined modes preserved on round-trip, and permission-aware
  visibility built on cache-skew-safe helpers shared through auth.js.
- Refused live-registry swaps surface as an amber registry_warning on
  the write, delete, reload, and calibrate responses; audit rows carry
  auth_gated / auth_disarmed markers visible in the audit view.

Registry and sessions:
- The encryption-key requirement for dynamic auth is enforced inside
  ModelRegistry.reload() itself — nodes refuse with 503 and the
  console records coord_registry_error — and reload bumps the
  generation before the map swap so a racing reader can never pair a
  stale generation with new maps.
- resolve()/resolve_binding() return the generation from inside the
  registry lock; sessions rebind per send on generation change with
  atomic client/provider/config commits, fallback-first handling of
  removed or unconstructable aliases, and judge/limiter resets only
  when the binding actually changed.
- Mint refusals record per-user causes surfaced in the per-turn
  heartbeat logs; misconfiguration warnings are deduplicated with
  bounded state.

Verification: 10417 tests (99 added on this branch), a 71-scenario
browser harness over the real admin shelf, and a live rfc8693
token-exchange e2e run (MCP legs verified end to end; the model-leg
scope gap is tracked as #955 under a narrow known-gap signature).

Closes #950.
2026-08-03 20:11:28 -07:00

153 lines
6.4 KiB
Python

"""Calibration core — probe a fake reranker and recommend a 0-1 floor."""
from __future__ import annotations
from turnstone.core.rerank import RerankHit
from turnstone.core.rerank_calibrate import _GAP_FRACTION, _PROBE_SET, _build_result, calibrate
class _ScriptedClient:
"""RerankClient stub: scores doc 0 (the relevant one) at ``r``, the rest ``i``.
Drives the real ``calibrate`` loop through the ``RerankClient`` seam — the
relevant doc is always position 0 of the documents calibrate sends.
"""
def __init__(self, r: float, i: float) -> None:
self._r, self._i = r, i
def rerank(
self, query: str, documents: list[str], *, top_n: int | None = None
) -> list[RerankHit]:
assert top_n is None # calibration must request every doc's score
return [RerankHit(index=0, score=self._r)] + [
RerankHit(index=idx, score=self._i) for idx in range(1, len(documents))
]
class _FlakyClient:
"""Fails the first ``cold`` calls (cold-start compile), then scores normally."""
def __init__(self, cold: int, r: float, i: float) -> None:
self.calls = 0
self.cold = cold
self._r, self._i = r, i
def rerank(
self, query: str, documents: list[str], *, top_n: int | None = None
) -> list[RerankHit]:
self.calls += 1
if self.calls <= self.cold:
raise RuntimeError("cold endpoint (compiling)")
return [RerankHit(index=0, score=self._r)] + [
RerankHit(index=idx, score=self._i) for idx in range(1, len(documents))
]
class TestCalibrate:
def test_warmup_absorbs_cold_start(self):
# First 2 calls fail (compile); warmup consumes them so the probe loop is
# warm and calibration still succeeds.
c = _FlakyClient(cold=2, r=0.9, i=0.1)
res = calibrate(c, model="m")
assert res.separated
assert c.calls > 2 # warmup absorbed the cold calls before the probes ran
def test_probability_scale_clean_separation(self):
res = calibrate(_ScriptedClient(0.9, 0.1), model="m")
assert res.raw_scale == "probability (0-1)" # already 0-1 -> identity
assert res.separated
# gap (0.1, 0.9); _GAP_FRACTION in from the irrelevant edge.
assert res.suggested_threshold == round(0.1 + _GAP_FRACTION * 0.8, 4)
assert res.irrelevant_max < res.suggested_threshold < res.relevant_min
assert res.n_relevant == len(_PROBE_SET)
assert res.n_irrelevant == len(_PROBE_SET) * (len(_PROBE_SET) - 1)
def test_logit_scale_normalized_then_separated(self):
# Out-of-[0,1] raw scores -> sigmoid -> a 0-1 floor regardless of scale.
res = calibrate(_ScriptedClient(5.0, -2.0), model="m")
assert "logit" in res.raw_scale
assert res.separated
assert res.suggested_threshold is not None
assert 0.0 < res.suggested_threshold < 1.0
assert res.irrelevant_max < res.suggested_threshold < res.relevant_min
# all reported score fields live in the normalised 0-1 space
assert 0.0 <= res.irrelevant_min <= res.relevant_max <= 1.0
def test_overlap_reports_no_separation(self):
# relevant 0.4 <= irrelevant 0.6 -> not separable, no recommendation.
res = calibrate(_ScriptedClient(0.4, 0.6), model="m")
assert not res.separated
assert res.suggested_threshold is None
def test_recall_bias_floor_below_lowest_relevant(self):
# The floor must never exceed the lowest relevant score (no false drops).
res = calibrate(_ScriptedClient(0.55, 0.45), model="m")
assert res.separated
assert res.suggested_threshold is not None
assert res.suggested_threshold < res.relevant_min
def test_empty_scores_is_no_separation(self):
# A broken endpoint that scores nothing -> health-check fail, no floor.
res = _build_result("m", "unknown (no scores)", [], [])
assert not res.separated
assert res.suggested_threshold is None
assert res.raw_scale == "unknown (no scores)"
class TestCalibrationCapsConfinement:
def test_calibrate_merge_confined_to_calibration_fields(self):
"""The merge touches only the three probe-derived keys and preserves
everything else in the gated ``capabilities`` column."""
import json
from turnstone.core.rerank_calibrate import (
calibration_caps_fields,
merge_calibration_into_caps,
)
result = _build_result("m", "probability (0-1)", [0.9, 0.95], [0.1, 0.2])
fields = calibration_caps_fields(result)
assert set(fields) == {"rerank_threshold", "rerank_scale", "rerank_separated"}
existing = {
"server_compat": {"api_surface": "chat", "extra_body": {"x": 1}},
"context_window": 5,
}
merged = json.loads(merge_calibration_into_caps(json.dumps(existing), result))
assert merged["server_compat"] == existing["server_compat"]
assert merged["context_window"] == 5
assert set(merged) == set(existing) | set(fields)
def test_confinement_refuses_type_flip_that_python_equality_masks(self):
"""Python ``!=`` conflates ``True`` with ``1``; the confinement
compare canonicalizes per key like the write gate's comparator."""
import json
from turnstone.core.rerank_calibrate import (
calibration_caps_fields,
calibration_confinement_violations,
)
result = _build_result("m", "probability (0-1)", [0.9, 0.95], [0.1, 0.2])
stored = json.dumps({"server_compat": {"stream": 1}})
merged = json.dumps({"server_compat": {"stream": True}, **calibration_caps_fields(result)})
assert calibration_confinement_violations(stored, merged, result) == ["server_compat"]
def test_confinement_ignores_integral_float_spelling(self):
"""``1.0`` vs ``1`` is JSON round-trip spelling, not a value change,
so a healthy merge is not refused over it."""
import json
from turnstone.core.rerank_calibrate import (
calibration_caps_fields,
calibration_confinement_violations,
)
result = _build_result("m", "probability (0-1)", [0.9, 0.95], [0.1, 0.2])
stored = json.dumps({"server_compat": {"scale": 1.0}})
merged = json.dumps({"server_compat": {"scale": 1}, **calibration_caps_fields(result)})
assert calibration_confinement_violations(stored, merged, result) == []