4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
209 lines
7.7 KiB
Python
209 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
|
|
import keyring
|
|
|
|
import config.llm_credentials as llm_credentials
|
|
import config.llm_keyring as llm_keyring
|
|
from config.llm_auth.credentials import (
|
|
has_llm_api_key,
|
|
llm_api_key_source,
|
|
resolve_for_request,
|
|
status,
|
|
)
|
|
from config.llm_auth.records import save_provider_auth_record
|
|
from tests.shared.keyring_backend import MemoryKeyring
|
|
|
|
|
|
class _MacOSKeyringBackend:
|
|
pass
|
|
|
|
|
|
_MacOSKeyringBackend.__module__ = "keyring.backends.macOS"
|
|
|
|
|
|
def _security_tool_path(name: str) -> str:
|
|
return f"/usr/bin/{name}"
|
|
|
|
|
|
def _darwin_platform() -> str:
|
|
return "Darwin"
|
|
|
|
|
|
def test_resolve_env_credential_prefers_env_over_keyring(monkeypatch) -> None:
|
|
monkeypatch.setenv("GITLAB_ACCESS_TOKEN", "from-env")
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
|
|
previous_backend = keyring.get_keyring()
|
|
keyring.set_keyring(MemoryKeyring())
|
|
try:
|
|
llm_credentials.save_llm_api_key("GITLAB_ACCESS_TOKEN", "from-keyring")
|
|
assert llm_credentials.resolve_env_credential("GITLAB_ACCESS_TOKEN") == "from-env"
|
|
finally:
|
|
keyring.set_keyring(previous_backend)
|
|
|
|
|
|
def test_unmanaged_llm_api_key_source_reports_env_keyring_and_none(monkeypatch) -> None:
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
monkeypatch.delenv("EXPERIMENTAL_API_KEY", raising=False)
|
|
|
|
previous_backend = keyring.get_keyring()
|
|
keyring.set_keyring(MemoryKeyring())
|
|
try:
|
|
assert llm_api_key_source("EXPERIMENTAL_API_KEY") == "none"
|
|
llm_credentials.save_llm_api_key("EXPERIMENTAL_API_KEY", "from-keyring")
|
|
assert llm_api_key_source("EXPERIMENTAL_API_KEY") == "keyring"
|
|
monkeypatch.setenv("EXPERIMENTAL_API_KEY", "from-env")
|
|
assert llm_api_key_source("EXPERIMENTAL_API_KEY") == "env"
|
|
finally:
|
|
keyring.set_keyring(previous_backend)
|
|
|
|
|
|
def test_managed_llm_api_key_source_uses_metadata_without_reading_secret(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
|
|
monkeypatch.setattr(llm_keyring.platform, "system", _darwin_platform)
|
|
monkeypatch.setattr(llm_keyring.shutil, "which", _security_tool_path)
|
|
monkeypatch.setattr(llm_keyring.keyring, "get_keyring", _MacOSKeyringBackend)
|
|
|
|
def _run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
assert command == [
|
|
"/usr/bin/security",
|
|
"find-generic-password",
|
|
"-s",
|
|
"opensre.llm",
|
|
"-a",
|
|
"OPENAI_API_KEY",
|
|
]
|
|
assert kwargs["check"] is False
|
|
return subprocess.CompletedProcess(command, 0)
|
|
|
|
def _get_password(_service: str, _username: str) -> str:
|
|
raise AssertionError("metadata source check must not read the keychain secret")
|
|
|
|
monkeypatch.setattr(llm_keyring.subprocess, "run", _run)
|
|
monkeypatch.setattr(llm_keyring.keyring, "get_password", _get_password)
|
|
save_provider_auth_record(
|
|
provider="openai",
|
|
auth_name="openai",
|
|
kind="api_key",
|
|
source="keyring",
|
|
detail="OPENAI_API_KEY stored in the system keychain.",
|
|
env_var="OPENAI_API_KEY",
|
|
)
|
|
|
|
assert llm_api_key_source("OPENAI_API_KEY") == "metadata"
|
|
assert has_llm_api_key("OPENAI_API_KEY") is True
|
|
|
|
|
|
def test_managed_missing_metadata_reports_none_without_reading_secret(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
|
|
monkeypatch.setattr(llm_keyring.platform, "system", _darwin_platform)
|
|
monkeypatch.setattr(llm_keyring.shutil, "which", _security_tool_path)
|
|
monkeypatch.setattr(llm_keyring.keyring, "get_keyring", _MacOSKeyringBackend)
|
|
|
|
def _run(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.CompletedProcess(command, 44)
|
|
|
|
def _get_password(_service: str, _username: str) -> str:
|
|
raise AssertionError("metadata source check must not read the keychain secret")
|
|
|
|
monkeypatch.setattr(llm_keyring.subprocess, "run", _run)
|
|
monkeypatch.setattr(llm_keyring.keyring, "get_password", _get_password)
|
|
|
|
assert llm_api_key_source("OPENAI_API_KEY") == "none"
|
|
assert has_llm_api_key("OPENAI_API_KEY") is False
|
|
|
|
|
|
def test_request_resolution_marks_deleted_keychain_metadata_stale(monkeypatch, tmp_path) -> None:
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
|
monkeypatch.setenv("OPENSRE_LLM_AUTH_METADATA_PATH", str(tmp_path / "llm-auth.json"))
|
|
save_provider_auth_record(
|
|
provider="deepseek",
|
|
auth_name="deepseek",
|
|
kind="api_key",
|
|
source="keyring",
|
|
detail="DEEPSEEK_API_KEY stored in the system keychain.",
|
|
env_var="DEEPSEEK_API_KEY",
|
|
)
|
|
|
|
previous_backend = keyring.get_keyring()
|
|
keyring.set_keyring(MemoryKeyring())
|
|
try:
|
|
before = status("deepseek")
|
|
resolution = resolve_for_request("deepseek")
|
|
after = status("deepseek")
|
|
finally:
|
|
keyring.set_keyring(previous_backend)
|
|
|
|
assert before.configured is True
|
|
assert before.stale is False
|
|
assert resolution.ok is False
|
|
assert after.configured is True
|
|
assert after.stale is True
|
|
assert after.verified is False
|
|
assert "Missing credential" in after.detail
|
|
|
|
|
|
def test_llm_credential_record_round_trips_in_keyring(monkeypatch) -> None:
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
|
|
previous_backend = keyring.get_keyring()
|
|
keyring.set_keyring(MemoryKeyring())
|
|
try:
|
|
llm_credentials.save_llm_credential_record(
|
|
"provider-auth:deepseek",
|
|
{"provider": "deepseek", "source": "keyring", "empty": ""},
|
|
)
|
|
|
|
assert llm_credentials.resolve_llm_credential_record("provider-auth:deepseek") == {
|
|
"provider": "deepseek",
|
|
"source": "keyring",
|
|
}
|
|
|
|
llm_credentials.delete_llm_credential_record("provider-auth:deepseek")
|
|
assert llm_credentials.resolve_llm_credential_record("provider-auth:deepseek") == {}
|
|
finally:
|
|
keyring.set_keyring(previous_backend)
|
|
|
|
|
|
def test_get_keyring_setup_instructions_for_linux_without_gnome_keyring(monkeypatch) -> None:
|
|
backend_class = type("Keyring", (), {})
|
|
backend_class.__module__ = "keyring.backends.fail"
|
|
|
|
monkeypatch.delenv("OPENSRE_DISABLE_KEYRING", raising=False)
|
|
monkeypatch.delenv("DBUS_SESSION_BUS_ADDRESS", raising=False)
|
|
monkeypatch.setattr(llm_keyring.platform, "system", lambda: "Linux")
|
|
monkeypatch.setattr(llm_keyring.shutil, "which", lambda _name: None)
|
|
monkeypatch.setattr(llm_keyring.keyring, "get_keyring", lambda: backend_class())
|
|
|
|
lines = llm_credentials.get_keyring_setup_instructions("ANTHROPIC_API_KEY")
|
|
|
|
assert lines[0] == "Current keyring backend: keyring.backends.fail.Keyring."
|
|
assert "missing the GNOME Keyring daemon" in lines[1]
|
|
assert any(
|
|
"sudo apt update && sudo apt install -y gnome-keyring dbus-user-session" in line
|
|
for line in lines
|
|
)
|
|
assert any("dbus-run-session -- sh" in line for line in lines)
|
|
|
|
|
|
def test_get_keyring_setup_instructions_when_keyring_is_disabled(monkeypatch) -> None:
|
|
monkeypatch.setenv("OPENSRE_DISABLE_KEYRING", "1")
|
|
|
|
lines = llm_credentials.get_keyring_setup_instructions("OPENAI_API_KEY")
|
|
|
|
assert lines == (
|
|
"Secure local credential storage is disabled by OPENSRE_DISABLE_KEYRING.",
|
|
"Unset OPENSRE_DISABLE_KEYRING and rerun `opensre onboard` to save OPENAI_API_KEY securely.",
|
|
)
|