Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions graphify/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,16 +272,27 @@ def _load_custom_providers() -> dict[str, dict]:
# the user's own global ~/.graphify/providers.json stays trusted.
local_path = _custom_providers_path(global_=False)
global_path = _custom_providers_path(global_=True)
# Running from $HOME makes the relative project-local path resolve to the
# global file itself. That is the user's own trusted config, not config
# injected by a repo, so it is not "local": warning about it is a false
# positive that only teaches people to export the bypass. On any stat
# error, resolve to "distinct" so the gate never silently relaxes.
try:
local_is_distinct = local_path.is_file() and not (
global_path.is_file() and local_path.samefile(global_path)
)
except OSError:
local_is_distinct = True
allow_local = os.environ.get("GRAPHIFY_ALLOW_LOCAL_PROVIDERS", "").strip().lower() in ("1", "true", "yes")
if local_path.is_file() and not allow_local:
if local_is_distinct and not allow_local:
print(
f"[graphify] WARNING: ignoring project-local {local_path} (custom providers control "
"where your corpus and API key are sent). Set GRAPHIFY_ALLOW_LOCAL_PROVIDERS=1 to load it.",
file=sys.stderr,
)

providers: dict[str, dict] = {}
paths = [local_path, global_path] if allow_local else [global_path]
paths = [local_path, global_path] if (allow_local and local_is_distinct) else [global_path]
for path in paths:
if path.is_file():
try:
Expand Down
24 changes: 24 additions & 0 deletions tests/test_provider_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,30 @@ def test_project_local_providers_loaded_with_optin(tmp_path, monkeypatch):
assert "lab" in loaded


def test_local_path_that_is_the_global_file_is_not_local(tmp_path, monkeypatch, capsys):
"""Running from $HOME, ./.graphify/providers.json IS ~/.graphify/providers.json.

The user's own trusted global file must load without the project-local
warning — a false positive there only trains people to export the
GRAPHIFY_ALLOW_LOCAL_PROVIDERS bypass and disarm the F1 gate for real.
"""
shared = tmp_path / ".graphify" / "providers.json"
shared.parent.mkdir()
shared.write_text(json.dumps({
"mine": {"base_url": "https://gateway.example/v1", "default_model": "m", "env_key": "K"}
}), encoding="utf-8")

from graphify import llm
monkeypatch.setattr(llm, "_custom_providers_path",
lambda global_=True: shared if global_ else tmp_path / ".graphify" / "providers.json")
monkeypatch.setattr(llm, "BACKENDS", {**llm.BACKENDS})
monkeypatch.delenv("GRAPHIFY_ALLOW_LOCAL_PROVIDERS", raising=False)

loaded = llm._load_custom_providers()
assert "mine" in loaded
assert "ignoring project-local" not in capsys.readouterr().err


def test_non_http_provider_base_url_rejected(tmp_path, monkeypatch):
"""A provider whose base_url uses a non-http(s) scheme is skipped on load (F1)."""
providers_file = tmp_path / "providers.json"
Expand Down