Skip to content

Commit d206ebd

Browse files
committed
feat: credential-signal retirement — explicit mode signal + server-reported threshold gate (nexus-9o6y2.13-.17)
RDR-188 P3, the R5 category-(c) designs. The client voyage key now has ZERO behavioral influence anywhere: .13 is_local_mode(): NEW explicit positive signal — pg_credentials presence (the record that a local service was provisioned) → local, above the legacy fallback, below service_url (a migrated local→managed install keeps the stale file; the configured endpoint wins). The legacy fallback shrinks to chroma-key-only; the voyage clause is DELETED (Gap 3: blanking the unconsumed key must never flip mode). A chroma-without-voyage install reclassifies local→cloud deliberately: half-configured cloud should surface its missing key, not silently go local. Regression pin: key blanking never changes the mode result. .14 _voyage_thresholds_active(): the service-mode leg consults the SERVER's reported embedder family — HttpVectorClient.embedding_mode() (GET /version, memoized per instance on success; failed probes return unknown WITHOUT memoizing so recovery re-probes). Thresholds on iff the engine says voyage. Client-key read deleted; process-global memo + reset_threshold_gate_cache_for_tests retired (the memo lives on the handle now). Mutation-verified: hardcoding the gate fails 2 tests. .15 supervisor plumb: SCOPE DECISION recorded in the plumb block — engine-bootstrap material per End State 1; engine-owned credential files rejected (same disk, spawn is already the trust boundary). .16 sweep: doctor's check-quotas voyage line is INFO-framed (bootstrap/ migration material, never a client-capability warning); mcp mode diagnostic tracks the new 4-branch precedence (+pg_credentials_found; voyage_key_found retained as explicitly-informational). .17 mode-lint tripwires: is_local_mode source must not read the voyage credential; the threshold gate must consult embedding_mode() and never get_credential. 200 tests green across the affected suites.
1 parent a3a7eaa commit d206ebd

10 files changed

Lines changed: 235 additions & 83 deletions

src/nexus/commands/doctor.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,9 +2144,16 @@ def _format_quota_report(report: dict) -> str:
21442144

21452145
# ── Voyage ───────────────────────────────────────────────────────────
21462146
v = report["voyage"]
2147-
status = _CHECK if v["api_key_set"] else _WARN
2148-
key_label = "VOYAGE_API_KEY: set" if v["api_key_set"] else "VOYAGE_API_KEY: absent"
2149-
lines.append(f" {status} Voyage AI: {key_label}")
2147+
# RDR-188 (nexus-9o6y2.16): the client key is engine-bootstrap/migration
2148+
# material — absence is INFO, not a warning (no client code path consumes
2149+
# it; the engine's own key state is what matters and doctor's service
2150+
# checks cover that).
2151+
key_label = (
2152+
"VOYAGE_API_KEY: set (engine-bootstrap/migration material)"
2153+
if v["api_key_set"]
2154+
else "VOYAGE_API_KEY: absent (client does not consume it; engine key plumbed at spawn)"
2155+
)
2156+
lines.append(f" {_CHECK} Voyage AI: {key_label}")
21502157
lines.append(f" target rpm (indexer rate limiter): {v['target_rpm']}")
21512158
for model, caps in v["models"].items():
21522159
lines.append(

src/nexus/config.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -582,9 +582,19 @@ def is_local_mode() -> bool:
582582
local (nexus-3k43p: the legacy heuristic below mis-detected a greenfield
583583
managed user — service_url set, no chroma/voyage key — as local). This
584584
mirrors ``_resolve_init_mode``'s precedence (NX_LOCAL wins over
585-
service_url, which wins over the legacy heuristic).
586-
- Otherwise (legacy, pre-6.0): True when **either** CHROMA_API_KEY or
587-
VOYAGE_API_KEY is absent.
585+
service_url, which wins over the rest). Wins over ``pg_credentials``
586+
too: a migrated local→managed install keeps the old file on disk.
587+
- ``pg_credentials`` present in the config dir → True — the EXPLICIT
588+
positive record that a local service was provisioned (``nx init``
589+
local mode; the same signal health.py gates its service checks on).
590+
RDR-188 P3.1 (nexus-9o6y2.13): mode comes from explicit install
591+
state, not key inference.
592+
- Otherwise (legacy, pre-service Chroma era): True when CHROMA_API_KEY
593+
is absent. The voyage clause is DELETED (RDR-188 Gap 3): the client
594+
no longer consumes the voyage key for anything, so its presence or
595+
absence must have ZERO mode influence — a chroma-key-without-voyage
596+
install is a half-configured CLOUD install whose missing key should
597+
surface loudly, never a silent flip to local.
588598
"""
589599
nx_local = os.environ.get("NX_LOCAL", "").strip()
590600
if nx_local == "1":
@@ -593,10 +603,12 @@ def is_local_mode() -> bool:
593603
return False
594604
if (get_credential("service_url") or "").strip():
595605
return False
596-
# Auto-detect (legacy): local mode when either cloud credential is missing
597-
chroma_key = get_credential("chroma_api_key")
598-
voyage_key = get_credential("voyage_api_key")
599-
return not (chroma_key and voyage_key)
606+
from nexus.db.pg_provision import CREDENTIALS_FILENAME # noqa: PLC0415 — leaf constant, deferred to keep config import-light
607+
608+
if (nexus_config_dir() / CREDENTIALS_FILENAME).is_file():
609+
return True
610+
# Auto-detect (legacy): a Chroma-Cloud key marks a cloud install.
611+
return not get_credential("chroma_api_key")
600612

601613

602614
# RDR-101 Phase 5c (nexus-o6aa.13) removed ``is_catalog_event_sourced``.

src/nexus/daemon/storage_service_daemon.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,15 @@ def _spawn_service(self) -> tuple[subprocess.Popen[bytes], int]:
537537
# credentials) so `nx daemon service start` works without manual env
538538
# plumbing. An explicit NX_VOYAGE_API_KEY in the caller's env wins.
539539
#
540+
# SCOPE DECISION (RDR-188 P3.3, bead nexus-9o6y2.15 — End State 1):
541+
# this plumb is ENGINE-BOOTSTRAP material, not client consumption.
542+
# The voyage key may persist in config.yml on local-service installs
543+
# SOLELY as the delivery wire for the engine's own embed+rerank key
544+
# at spawn time; zero client code paths consume it (enforced by the
545+
# tests/test_rdr188_tripwires.py suite). Engine-owned credential
546+
# files were considered and rejected: the key lives on the same disk
547+
# either way, and this spawn is already the trust boundary.
548+
#
540549
# nexus-r5f3c: the configured local embed model is the INTENT record.
541550
# `nx init` (local mode) provisions bge-768 and saves
542551
# ``local.embed_model``; plumbing an ambient VOYAGE_API_KEY anyway

src/nexus/db/http_vector_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,36 @@ def put(
991991
#: never asked to rerank.
992992
supports_server_rerank: bool = True
993993

994+
#: Memoized GET /version ``embedding_mode`` (class-level default so
995+
#: partially-constructed test instances still resolve; successful probes
996+
#: shadow it per-instance). RDR-188 P3.2 (nexus-9o6y2.14).
997+
_embedding_mode_memo: str | None = None
998+
999+
def embedding_mode(self) -> str | None:
1000+
"""The engine's AUTHORITATIVE embedder family from ``GET /version``:
1001+
``"voyage"`` or ``"onnx-local"`` (``EmbedderRouter.modeName``).
1002+
1003+
RDR-188 P3.2 (nexus-9o6y2.14): this replaces client-key inference as
1004+
the search threshold gate's signal — thresholds are calibrated for
1005+
Voyage embeddings, and whether Voyage served the query is a fact
1006+
about the SERVER, not about which keys sit in client config.
1007+
1008+
Memoized per instance on success (the engine's mode is fixed for its
1009+
process lifetime). A failed probe returns ``None`` WITHOUT memoizing —
1010+
unknown, not "not voyage" — so a service that was briefly unreachable
1011+
is re-asked on the next call rather than locking thresholds off.
1012+
"""
1013+
if self._embedding_mode_memo is None:
1014+
try:
1015+
info = _get("/version", tenant=self._tenant)
1016+
except Exception as exc: # noqa: BLE001 — probe is advisory; search itself surfaces a down service
1017+
_log.debug("embedding_mode_probe_failed", error=str(exc))
1018+
return None
1019+
mode = info.get("embedding_mode") if isinstance(info, dict) else None
1020+
if isinstance(mode, str) and mode:
1021+
self._embedding_mode_memo = mode
1022+
return self._embedding_mode_memo
1023+
9941024
def search(
9951025
self,
9961026
query: str,

src/nexus/mcp/core.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6532,13 +6532,18 @@ def _resolve_mode_diagnostics() -> dict[str, str | None]:
65326532
live Desktop run leaves a durable on-disk trail in mcp.log instead of
65336533
requiring the divergence to be reproduced first.
65346534
6535-
nexus-smd1k (substantive-critic finding): ``is_local_mode()`` has THREE
6536-
decision branches (explicit ``NX_LOCAL``, ``service_url`` presence,
6537-
legacy chroma/voyage-key presence) — per the bead's own CLI-vs-GUI
6538-
divergence evidence, the more likely root cause sits in branch 2 or 3,
6539-
not branch 1. ``service_url_found``/``chroma_key_found``/
6540-
``voyage_key_found`` evidence which credential each branch actually
6541-
saw, as booleans ONLY — never the credential values themselves.
6535+
nexus-smd1k (substantive-critic finding): ``is_local_mode()`` has FOUR
6536+
decision branches since RDR-188 P3.1 (explicit ``NX_LOCAL``,
6537+
``service_url`` presence, ``pg_credentials`` presence, legacy
6538+
chroma-key fallback) — per the bead's own CLI-vs-GUI divergence
6539+
evidence, the likely root cause sits below branch 1.
6540+
``service_url_found``/``pg_credentials_found``/``chroma_key_found``
6541+
evidence what each branch actually saw, as booleans ONLY — never the
6542+
credential values. ``voyage_key_found`` is retained as INFORMATIONAL
6543+
(nexus-9o6y2.16): since RDR-188 the voyage key has ZERO mode
6544+
influence — it is engine-bootstrap/migration material; the boolean
6545+
stays because a divergence report that shows it TRUE while mode
6546+
flips would immediately falsify a suspected key-inference regression.
65426547
65436548
Never raises: a diagnostic must not block MCP startup. On resolution
65446549
failure returns ``{"mode": "unknown", "error": str(exc)}``.
@@ -6559,7 +6564,9 @@ def _resolve_mode_diagnostics() -> dict[str, str | None]:
65596564
"home": _os.environ.get("HOME", ""),
65606565
"nx_local_env": _os.environ.get("NX_LOCAL", ""),
65616566
"service_url_found": bool(get_credential("service_url")),
6567+
"pg_credentials_found": (nexus_config_dir() / "pg_credentials").is_file(),
65626568
"chroma_key_found": bool(get_credential("chroma_api_key")),
6569+
# Informational only — zero mode influence since RDR-188 P3.1.
65636570
"voyage_key_found": bool(get_credential("voyage_api_key")),
65646571
}
65656572
except Exception as exc: # noqa: BLE001 — diagnostic-only, must never block startup

src/nexus/search_engine.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -307,37 +307,24 @@ def _voyage_thresholds_active(t3: Any) -> bool:
307307
that attribute, so filtering silently disabled on every service-mode
308308
search — the same bug class as the reranker (nexus-xbw0f).
309309
310-
The fallback preserves the original semantics for HttpVectorClient:
311-
Voyage is in use iff NOT local mode (local embeds with bge/MiniLM —
312-
Voyage-calibrated thresholds must stay off) AND a Voyage key is
313-
configured. Other handles (test fakes, injected stubs) keep the
310+
RDR-188 P3.2 (nexus-9o6y2.14): for HttpVectorClient the signal is the
311+
SERVER-reported embedder family (``t3.embedding_mode()`` from
312+
``GET /version``) — thresholds on iff the engine embeds with Voyage.
313+
The former client-key heuristic is retired with the client's Voyage
314+
consumption. Other handles (test fakes, injected stubs) keep the
314315
attribute-only gate.
315316
"""
316317
if getattr(t3, "_voyage_client", None) is not None:
317318
return True
318319
if not isinstance(t3, HttpVectorClient):
319320
return False
320-
global _service_thresholds_memo
321-
if _service_thresholds_memo is None:
322-
# Memoized for the process lifetime (wave review): is_local_mode +
323-
# get_credential each re-read config.yml, a per-search file-IO cost.
324-
# Mode/credential changes require a process restart — same lifetime
325-
# the retired T3Database gave its _voyage_client.
326-
from nexus.config import get_credential, is_local_mode # noqa: PLC0415 — circular-dep avoidance (config)
327-
328-
_service_thresholds_memo = (
329-
not is_local_mode() and bool(get_credential("voyage_api_key"))
330-
)
331-
return _service_thresholds_memo
332-
333-
334-
_service_thresholds_memo: bool | None = None
335-
336-
337-
def reset_threshold_gate_cache_for_tests() -> None:
338-
"""Clear the service-mode threshold-gate memo (test isolation)."""
339-
global _service_thresholds_memo
340-
_service_thresholds_memo = None
321+
# RDR-188 P3.2 (nexus-9o6y2.14): the gate consults the SERVER's reported
322+
# embedder family (GET /version, memoized on the client handle) — not the
323+
# client's credential file. Removing the now-unconsumed client voyage key
324+
# can no longer silently regress search quality (Gap 3), and the signal is
325+
# what actually embedded the corpus. Unknown (probe failed) → thresholds
326+
# off: never guess Voyage; a down service fails the search itself anyway.
327+
return t3.embedding_mode() == "voyage"
341328

342329

343330
def _overfetch_multiplier(collection_name: str) -> int:

tests/db/test_http_vector_client_rerank.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,40 @@ def test_no_rerank_request_body_and_return_unchanged(client, monkeypatch):
9696
assert "rerank" not in captured[0]["body"]
9797
assert "rerank_top_k" not in captured[0]["body"]
9898
assert rows[0]["id"] == "a"
99+
100+
101+
# ── RDR-188 P3.2 (nexus-9o6y2.14): server-reported embedding mode ────────────
102+
103+
104+
def test_embedding_mode_reads_version_and_memoizes(client, monkeypatch):
105+
calls: list[str] = []
106+
107+
def fake_get(path, tenant=None):
108+
calls.append(path)
109+
return {"embedding_mode": "voyage", "embedding_models": ["voyage-code-3"]}
110+
111+
monkeypatch.setattr(hvc, "_get", fake_get)
112+
assert client.embedding_mode() == "voyage"
113+
assert client.embedding_mode() == "voyage"
114+
assert calls == ["/version"] # memoized after first success
115+
116+
117+
def test_embedding_mode_probe_failure_returns_none_without_memoizing(client, monkeypatch):
118+
attempts: list[int] = []
119+
120+
def failing_get(path, tenant=None):
121+
attempts.append(1)
122+
raise ConnectionError("service down")
123+
124+
monkeypatch.setattr(hvc, "_get", failing_get)
125+
assert client.embedding_mode() is None
126+
# Not memoized: recovery is re-probed, thresholds are not locked off.
127+
monkeypatch.setattr(hvc, "_get",
128+
lambda path, tenant=None: {"embedding_mode": "onnx-local"})
129+
assert client.embedding_mode() == "onnx-local"
130+
assert attempts == [1]
131+
132+
133+
def test_embedding_mode_garbage_version_body_is_unknown(client, monkeypatch):
134+
monkeypatch.setattr(hvc, "_get", lambda path, tenant=None: ["not", "a", "dict"])
135+
assert client.embedding_mode() is None

tests/test_local_mode.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ class TestIsLocalMode:
4949
pytest.param({"NX_LOCAL": "0"}, False, id="nx_local_0_overrides"),
5050
pytest.param({}, True, id="no_credentials"),
5151
pytest.param({"CHROMA_API_KEY": "k", "VOYAGE_API_KEY": "k"}, False, id="both_keys"),
52-
pytest.param({"CHROMA_API_KEY": "k"}, True, id="chroma_only"),
52+
# RDR-188 P3.1 (nexus-9o6y2.13): the legacy fallback is chroma-only.
53+
# A chroma key marks a Chroma-Cloud install even with no voyage key
54+
# (half-configured cloud surfaces its missing key loudly instead of
55+
# silently flipping local).
56+
pytest.param({"CHROMA_API_KEY": "k"}, False, id="chroma_only_is_cloud"),
5357
pytest.param({"VOYAGE_API_KEY": "k"}, True, id="voyage_only"),
5458
# nexus-3k43p: a managed 6.0 user (service_url set, no chroma/voyage
5559
# key) must NOT be mis-detected as local. service_url presence wins
@@ -68,8 +72,55 @@ def test_is_local_mode(
6872
for k, v in env.items():
6973
monkeypatch.setenv(k, v)
7074
monkeypatch.setenv("HOME", str(tmp_path))
75+
monkeypatch.setenv("NEXUS_CONFIG_DIR", str(tmp_path / "cfg"))
7176
assert is_local_mode() is expected
7277

78+
def _cfg(self, tmp_path, monkeypatch, *, pg_creds: bool):
79+
cfg = tmp_path / "cfg"
80+
cfg.mkdir(exist_ok=True)
81+
monkeypatch.setenv("NEXUS_CONFIG_DIR", str(cfg))
82+
monkeypatch.setenv("HOME", str(tmp_path))
83+
for var in ("NX_LOCAL", "CHROMA_API_KEY", "VOYAGE_API_KEY", "NX_SERVICE_URL"):
84+
monkeypatch.delenv(var, raising=False)
85+
if pg_creds:
86+
from nexus.db.pg_provision import CREDENTIALS_FILENAME
87+
(cfg / CREDENTIALS_FILENAME).write_text("PGHOST=127.0.0.1\n")
88+
89+
# ── RDR-188 P3.1 (nexus-9o6y2.13): explicit local signal + no voyage clause ──
90+
91+
def test_pg_credentials_is_the_explicit_local_signal(self, tmp_path, monkeypatch):
92+
"""A provisioned local service (pg_credentials present) is LOCAL even
93+
when legacy cloud keys sit in the environment (they are migration-
94+
source / engine-bootstrap material, not mode signals)."""
95+
self._cfg(tmp_path, monkeypatch, pg_creds=True)
96+
monkeypatch.setenv("CHROMA_API_KEY", "k")
97+
monkeypatch.setenv("VOYAGE_API_KEY", "k")
98+
assert is_local_mode() is True
99+
100+
def test_service_url_beats_pg_credentials(self, tmp_path, monkeypatch):
101+
"""A migrated local→managed install keeps its old pg_credentials on
102+
disk; the configured service_url must win (managed, not local)."""
103+
self._cfg(tmp_path, monkeypatch, pg_creds=True)
104+
monkeypatch.setenv("NX_SERVICE_URL", "https://m.example")
105+
assert is_local_mode() is False
106+
107+
def test_blanking_voyage_key_never_changes_mode(self, tmp_path, monkeypatch):
108+
"""Gap 3's regression pin: the client no longer consumes the voyage
109+
key (RDR-188), so its presence/absence must have ZERO mode influence
110+
in every branch."""
111+
self._cfg(tmp_path, monkeypatch, pg_creds=False)
112+
monkeypatch.setenv("CHROMA_API_KEY", "k")
113+
monkeypatch.setenv("VOYAGE_API_KEY", "k")
114+
with_key = is_local_mode()
115+
monkeypatch.delenv("VOYAGE_API_KEY")
116+
assert is_local_mode() is with_key
117+
118+
monkeypatch.delenv("CHROMA_API_KEY")
119+
monkeypatch.setenv("VOYAGE_API_KEY", "k")
120+
with_key_no_chroma = is_local_mode()
121+
monkeypatch.delenv("VOYAGE_API_KEY")
122+
assert is_local_mode() is with_key_no_chroma
123+
73124

74125
class TestDefaultLocalPath:
75126
@pytest.mark.parametrize(

tests/test_rdr188_tripwires.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,35 @@ def fake(q, cols, n_results, t3, where=None, *, rerank=False,
228228
assert res.exit_code == 0, res.output
229229
assert "server rerank degraded" in res.output
230230
assert "engine says no" in res.output
231+
232+
233+
# ── 5. P3 mode-lint (nexus-9o6y2.17): retired voyage heuristics stay gone ───
234+
235+
236+
def test_is_local_mode_source_has_no_voyage_clause():
237+
""".13's deletion, pinned at source level: the mode decision must never
238+
again read the voyage credential."""
239+
src = (_SRC / "config.py").read_text()
240+
start = src.index("def is_local_mode(")
241+
end = src.index("\ndef ", start + 10)
242+
body = src[start:end]
243+
# The docstring legitimately EXPLAINS the deletion; the lint targets the
244+
# credential-read token itself.
245+
assert 'get_credential("voyage' not in body, (
246+
"is_local_mode() reads the voyage credential again — RDR-188 P3.1 "
247+
"deleted key inference from the mode decision"
248+
)
249+
250+
251+
def test_threshold_gate_source_reads_server_mode_not_client_key():
252+
""".14's rewrite, pinned: the service-mode threshold gate consults the
253+
server's embedding_mode, never the client credential file."""
254+
src = (_SRC / "search_engine.py").read_text()
255+
start = src.index("def _voyage_thresholds_active(")
256+
end = src.index("\ndef ", start + 10)
257+
body = src[start:end]
258+
assert "embedding_mode()" in body
259+
assert "get_credential" not in body, (
260+
"_voyage_thresholds_active reads client credentials again — the "
261+
"signal is the SERVER's reported embedder family (RDR-188 P3.2)"
262+
)

0 commit comments

Comments
 (0)