Activation reaches plugin discovery before the application dependencies exist. Give PM its own locked Python project and runtime so it can install or repair the application without importing that dependency tree. Keep PM outside the application workspace. A shared uv workspace resolves the application graph and cannot provide this isolation. Route mutations through an isolated worker and preserve transaction callbacks, cancellation, custom package registrations, and correlated receipts. Use the same runtime builder for source installs and packaged payloads. Keep offline wheelhouse support in that builder. Nix builds the independent PM lock as a separate derivation. Refuse lazy-disabled bootstrap before installing tools or dependencies. Move first-party YAML readers and writers to ruamel. Keep the application lock's transitive PyYAML requirements for third-party packages. Verification: - Focused canonical Python suite: 177 passed, 1 host-gated skip. - Electron backend probes: 12 passed. Electron typecheck passed. - Both uv locks, scoped lint, Bash syntax, and whitespace checks passed. - Cold activation, corrupt-app repair, offline staging, and relocation ran. - Built and exercised the Nix PM runtime and standalone YAML merge script. Six broader caller test files retain the same 24 failing test IDs as an archive of HEAD. The existing real-home guard blocks those tests before they can exercise the affected paths. No full-suite pass is claimed. Native Windows signing and full Bionic package execution remain unverified.
108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
"""Bedrock API-key setup must produce a config that actually authenticates.
|
|
|
|
The wizard used to stash the Bedrock bearer token in ``OPENAI_API_KEY`` and set
|
|
a bare ``provider: custom``. Since the cross-provider credential gate landed
|
|
(#28660) that variable is only honoured for ``openai.com`` hosts, so the token
|
|
was silently dropped and every request went out as ``no-key-required``.
|
|
|
|
These tests lock the seam the bug lived in: what the wizard writes, and what the
|
|
runtime resolver then makes of it.
|
|
"""
|
|
|
|
import os
|
|
|
|
import hermes_yaml as yaml
|
|
|
|
import hermes_cli.runtime_provider as rp
|
|
from hermes_cli.model_setup_flows_bedrock import _model_flow_bedrock_api_key
|
|
|
|
|
|
REGION = "us-east-1"
|
|
TOKEN = "test-bedrock-bearer-token"
|
|
|
|
|
|
def _run_wizard(monkeypatch, selected="openai.gpt-5.6-terra"):
|
|
"""Drive the real setup flow non-interactively and return the saved config."""
|
|
import hermes_cli.auth as auth_mod
|
|
|
|
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", TOKEN)
|
|
monkeypatch.setattr(
|
|
auth_mod, "_prompt_model_selection", lambda *a, **k: selected
|
|
)
|
|
monkeypatch.setattr(auth_mod, "_save_model_choice", lambda *a, **k: None)
|
|
monkeypatch.setattr(auth_mod, "deactivate_provider", lambda *a, **k: None)
|
|
|
|
_model_flow_bedrock_api_key({}, REGION)
|
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
return get_hermes_home(), yaml.safe_load(
|
|
(get_hermes_home() / "config.yaml").read_text(encoding="utf-8")
|
|
)
|
|
|
|
|
|
def test_wizard_writes_named_provider_carrying_the_key_env(monkeypatch):
|
|
home, cfg = _run_wizard(monkeypatch)
|
|
|
|
# The credential must travel via a named provider entry: that is the only
|
|
# resolution branch that reads key_env.
|
|
entry = cfg["providers"]["bedrock-mantle"]
|
|
assert entry["key_env"] == "AWS_BEARER_TOKEN_BEDROCK"
|
|
assert entry["base_url"].startswith(f"https://bedrock-mantle.{REGION}.api.aws")
|
|
assert cfg["model"]["provider"] == "custom:bedrock-mantle"
|
|
|
|
# A bare ``custom`` provider plus model.base_url is the shape that could not
|
|
# carry the token; make sure we did not leave it behind.
|
|
assert "base_url" not in cfg["model"]
|
|
|
|
|
|
def test_wizard_does_not_park_the_bedrock_token_in_openai_api_key(monkeypatch):
|
|
home, _cfg = _run_wizard(monkeypatch)
|
|
|
|
env_file = home / ".env"
|
|
written = env_file.read_text(encoding="utf-8") if env_file.exists() else ""
|
|
names = {
|
|
line.split("=", 1)[0].strip()
|
|
for line in written.splitlines()
|
|
if "=" in line and not line.lstrip().startswith("#")
|
|
}
|
|
|
|
# Writing a Bedrock credential into another vendor's variable is what the
|
|
# #28660 gate exists to prevent. (The token itself already came from the
|
|
# environment here, so the flow has no reason to re-write it.)
|
|
assert "OPENAI_API_KEY" not in names
|
|
assert "OPENAI_BASE_URL" not in names
|
|
|
|
|
|
def test_saved_config_resolves_the_bearer_token_not_a_placeholder(monkeypatch):
|
|
"""The regression contract: the token must reach the resolved runtime."""
|
|
_home, cfg = _run_wizard(monkeypatch)
|
|
|
|
# Nothing else may supply a credential for this host.
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", TOKEN)
|
|
monkeypatch.setattr(rp, "load_config", lambda: cfg)
|
|
|
|
resolved = rp.resolve_runtime_provider(
|
|
requested=cfg["model"]["provider"],
|
|
)
|
|
|
|
assert resolved["api_key"] == TOKEN
|
|
assert resolved["api_key"] != "no-key-required"
|
|
assert "bedrock-mantle" in resolved["base_url"]
|
|
|
|
|
|
def test_resolution_fails_closed_when_the_token_is_absent(monkeypatch):
|
|
"""No token in the environment must not silently resolve to a placeholder key."""
|
|
_home, cfg = _run_wizard(monkeypatch)
|
|
|
|
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
monkeypatch.setattr(rp, "load_config", lambda: cfg)
|
|
|
|
resolved = rp.resolve_runtime_provider(requested=cfg["model"]["provider"])
|
|
|
|
assert resolved["api_key"] != TOKEN
|