"""Tests for MUSE_AGENT_HD_SEED injection in get_signing_identity. Covers the new HD sub-seed resolution step (step 1 in the resolution order) added to muse.cli.config.get_signing_identity during Phase 2. All eight categories: 1. Unit — base64url decode, 64-byte validation, key materialisation 2. Integration — get_signing_identity with MUSE_AGENT_HD_SEED set 3. E2E — full derivation → signing round-trip 4. Stress — 100 identity resolutions from HD seed 5. Data integrity — deterministic key, handle resolution order 6. Performance — resolution completes within budget 7. Security — wrong-length seed ignored, malformed seed ignored, no fallthrough to MUSE_AGENT_KEY on bad seed 8. Docstrings — get_signing_identity docstring mentions MUSE_AGENT_HD_SEED """ from __future__ import annotations import base64 import time import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # --------------------------------------------------------------------------- # Constants — fixed test mnemonic (never used in production) # --------------------------------------------------------------------------- _TEST_MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_hd_seed(account: int = 1) -> bytes: """Derive a real 64-byte IDENTITY-domain agent sub-seed.""" from muse.core.bip39 import mnemonic_to_seed from muse.core.hdkeys import DOMAIN_IDENTITY, derive_agent_sub_seed seed = mnemonic_to_seed(_TEST_MNEMONIC) return derive_agent_sub_seed(seed, domain=DOMAIN_IDENTITY, agent_id=account) def _encode_hd_seed(sub_seed: bytes) -> str: """base64url-encode a sub-seed (no padding, as the env var format requires).""" return base64.urlsafe_b64encode(sub_seed).rstrip(b"=").decode() # --------------------------------------------------------------------------- # 1. Unit — low-level building blocks # --------------------------------------------------------------------------- class TestHdSeedDecode: """Unit tests for sub-seed decode/validation logic.""" def test_64_byte_seed_decodes_correctly(self) -> None: raw = _make_hd_seed(1) encoded = _encode_hd_seed(raw) decoded = base64.urlsafe_b64decode(encoded + "==") assert decoded == raw def test_non_64_byte_seed_identified(self) -> None: short = b"\x00" * 32 encoded = _encode_hd_seed(short) decoded = base64.urlsafe_b64decode(encoded + "==") assert len(decoded) == 32 # not 64 → should be ignored by get_signing_identity def test_derive_identity_key_from_sub_seed(self) -> None: from muse.core.hdkeys import derive_identity_key, dk_to_ed25519 sub_seed = _make_hd_seed(1) dk = derive_identity_key(sub_seed) priv = dk_to_ed25519(dk) assert isinstance(priv, Ed25519PrivateKey) def test_public_key_is_32_bytes(self) -> None: from muse.core.hdkeys import derive_identity_key, dk_to_ed25519 sub_seed = _make_hd_seed(1) dk = derive_identity_key(sub_seed) priv = dk_to_ed25519(dk) pub = priv.public_key().public_bytes_raw() assert len(pub) == 32 # --------------------------------------------------------------------------- # 2. Integration — get_signing_identity with MUSE_AGENT_HD_SEED # --------------------------------------------------------------------------- class TestGetSigningIdentityHdSeed: """Integration tests for the MUSE_AGENT_HD_SEED resolution path.""" def test_returns_signing_identity_when_hd_seed_set( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) encoded = _encode_hd_seed(sub_seed) monkeypatch.setenv("MUSE_AGENT_HD_SEED", encoded) result = get_signing_identity() assert result is not None def test_result_has_private_key(self, monkeypatch: pytest.MonkeyPatch) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(2) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) identity = get_signing_identity() assert hasattr(identity, "private_key") assert isinstance(identity.private_key, Ed25519PrivateKey) def test_result_handle_defaults_to_agent( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) monkeypatch.delenv("MUSE_AGENT_HANDLE", raising=False) identity = get_signing_identity() assert identity.handle == "agent" def test_result_handle_from_muse_agent_handle( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) monkeypatch.setenv("MUSE_AGENT_HANDLE", "orchestra-bot") identity = get_signing_identity() assert identity.handle == "orchestra-bot" def test_result_handle_falls_back_to_agent_id_param( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) monkeypatch.delenv("MUSE_AGENT_HANDLE", raising=False) identity = get_signing_identity(agent_id="run-abc123") assert identity.handle == "run-abc123" def test_hd_seed_takes_priority_over_muse_agent_key( self, monkeypatch: pytest.MonkeyPatch ) -> None: """MUSE_AGENT_HD_SEED must win over MUSE_AGENT_KEY (step 1 > step 2).""" from muse.cli.config import get_signing_identity from muse.core.hdkeys import derive_identity_key, dk_to_ed25519 sub_seed = _make_hd_seed(1) encoded = _encode_hd_seed(sub_seed) # Derive expected public key from HD seed dk = derive_identity_key(sub_seed) expected_priv = dk_to_ed25519(dk) expected_pub = expected_priv.public_key().public_bytes_raw() # Also set a different MUSE_AGENT_KEY (should be ignored) other_key = Ed25519PrivateKey.generate() from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, ) other_pem = other_key.private_bytes( Encoding.PEM, PrivateFormat.PKCS8, NoEncryption() ).decode() monkeypatch.setenv("MUSE_AGENT_HD_SEED", encoded) monkeypatch.setenv("MUSE_AGENT_KEY", other_pem) identity = get_signing_identity() actual_pub = identity.private_key.public_key().public_bytes_raw() assert actual_pub == expected_pub, ( "HD seed should win; got a different public key (MUSE_AGENT_KEY leaked through)" ) # --------------------------------------------------------------------------- # 3. E2E — full derivation → sign → verify round-trip # --------------------------------------------------------------------------- class TestSignVerifyRoundtrip: """End-to-end: derive from mnemonic → set env var → resolve identity → sign.""" def test_sign_and_verify(self, monkeypatch: pytest.MonkeyPatch) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) identity = get_signing_identity() message = b"muse phase 2 hd seed e2e test" signature = identity.private_key.sign(message) # verify should not raise identity.private_key.public_key().verify(signature, message) def test_different_accounts_produce_different_signatures( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity message = b"same message" sub_seed_1 = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed_1)) id1 = get_signing_identity() sig1 = id1.private_key.sign(message) sub_seed_2 = _make_hd_seed(2) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed_2)) id2 = get_signing_identity() sig2 = id2.private_key.sign(message) assert sig1 != sig2 def test_same_account_same_signature( self, monkeypatch: pytest.MonkeyPatch ) -> None: """Deterministic: same seed → same key → same signature for same message.""" from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(5) message = b"determinism check" monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) id1 = get_signing_identity() sig1 = id1.private_key.sign(message) id2 = get_signing_identity() sig2 = id2.private_key.sign(message) assert sig1 == sig2 # --------------------------------------------------------------------------- # 4. Stress — repeated resolutions # --------------------------------------------------------------------------- class TestStress: """Stress tests.""" def test_100_resolutions_all_return_identity( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) identities = [get_signing_identity() for _ in range(100)] assert all(i is not None for i in identities) def test_100_resolutions_all_same_pubkey( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(3) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) pubs = { get_signing_identity().private_key.public_key().public_bytes_raw() for _ in range(100) } assert len(pubs) == 1, "Repeated resolution produced different public keys" # --------------------------------------------------------------------------- # 5. Data integrity # --------------------------------------------------------------------------- class TestDataIntegrity: """Data integrity tests.""" def test_public_key_matches_direct_derivation( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity from muse.core.hdkeys import derive_identity_key, dk_to_ed25519 sub_seed = _make_hd_seed(7) dk = derive_identity_key(sub_seed) expected_pub = dk_to_ed25519(dk).public_key().public_bytes_raw() monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) identity = get_signing_identity() actual_pub = identity.private_key.public_key().public_bytes_raw() assert actual_pub == expected_pub def test_with_padding_variants(self, monkeypatch: pytest.MonkeyPatch) -> None: """get_signing_identity must handle base64url with or without padding.""" from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) # Encode without padding (production format) no_pad = base64.urlsafe_b64encode(sub_seed).rstrip(b"=").decode() monkeypatch.setenv("MUSE_AGENT_HD_SEED", no_pad) identity = get_signing_identity() assert identity is not None def test_handle_preference_order(self, monkeypatch: pytest.MonkeyPatch) -> None: """MUSE_AGENT_HANDLE > agent_id param > 'agent' default.""" from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) # Neither set → "agent" monkeypatch.delenv("MUSE_AGENT_HANDLE", raising=False) assert get_signing_identity().handle == "agent" # agent_id set → that value assert get_signing_identity(agent_id="my-agent").handle == "my-agent" # MUSE_AGENT_HANDLE overrides agent_id monkeypatch.setenv("MUSE_AGENT_HANDLE", "env-handle") assert get_signing_identity(agent_id="ignored").handle == "env-handle" # --------------------------------------------------------------------------- # 6. Performance # --------------------------------------------------------------------------- class TestPerformance: """Performance tests.""" def test_single_resolution_under_2_seconds( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) start = time.monotonic() get_signing_identity() elapsed = time.monotonic() - start assert elapsed < 2.0, f"Single resolution took {elapsed:.3f}s" def test_10_resolutions_under_5_seconds( self, monkeypatch: pytest.MonkeyPatch ) -> None: from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) monkeypatch.setenv("MUSE_AGENT_HD_SEED", _encode_hd_seed(sub_seed)) start = time.monotonic() for _ in range(10): get_signing_identity() elapsed = time.monotonic() - start assert elapsed < 5.0, f"10 resolutions took {elapsed:.3f}s" # --------------------------------------------------------------------------- # 7. Security # --------------------------------------------------------------------------- class TestSecurity: """Security tests.""" def test_wrong_length_seed_ignored( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A 32-byte seed must be silently ignored — no identity returned from it.""" from muse.cli.config import get_signing_identity monkeypatch.chdir(tmp_path) # no config.toml → no hub fallback short = base64.urlsafe_b64encode(b"\x00" * 32).rstrip(b"=").decode() monkeypatch.setenv("MUSE_AGENT_HD_SEED", short) monkeypatch.delenv("MUSE_AGENT_KEY", raising=False) # Should return None (no hub configured, wrong-length seed ignored) result = get_signing_identity() assert result is None def test_malformed_base64_ignored( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Non-base64 content must be silently ignored.""" from muse.cli.config import get_signing_identity monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_AGENT_HD_SEED", "!!!not-base64!!!") monkeypatch.delenv("MUSE_AGENT_KEY", raising=False) result = get_signing_identity() assert result is None def test_empty_hd_seed_skips_to_next_step( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Empty MUSE_AGENT_HD_SEED should fall through to MUSE_AGENT_KEY.""" from muse.cli.config import get_signing_identity monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_AGENT_HD_SEED", "") # Provide a valid MUSE_AGENT_KEY so we can confirm fall-through happened good_key = Ed25519PrivateKey.generate() from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, ) pem = good_key.private_bytes( Encoding.PEM, PrivateFormat.PKCS8, NoEncryption() ).decode() monkeypatch.setenv("MUSE_AGENT_KEY", pem) monkeypatch.setenv("MUSE_AGENT_HANDLE", "fallthrough-agent") identity = get_signing_identity() assert identity is not None assert identity.handle == "fallthrough-agent" assert ( identity.private_key.public_key().public_bytes_raw() == good_key.public_key().public_bytes_raw() ) def test_hd_seed_env_var_not_logged_at_debug( self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: """The raw sub-seed bytes must never appear in log output.""" import logging from muse.cli.config import get_signing_identity sub_seed = _make_hd_seed(1) encoded = _encode_hd_seed(sub_seed) monkeypatch.setenv("MUSE_AGENT_HD_SEED", encoded) with caplog.at_level(logging.DEBUG, logger="muse.cli.config"): get_signing_identity() # The raw encoded seed must not appear verbatim in any log record for record in caplog.records: assert encoded not in record.getMessage() # --------------------------------------------------------------------------- # 8. Docstrings # --------------------------------------------------------------------------- class TestDocstrings: """Verify get_signing_identity documents the MUSE_AGENT_HD_SEED step.""" def test_get_signing_identity_mentions_hd_seed(self) -> None: from muse.cli.config import get_signing_identity doc = get_signing_identity.__doc__ or "" assert "MUSE_AGENT_HD_SEED" in doc, ( "get_signing_identity docstring must document the MUSE_AGENT_HD_SEED " "resolution step." ) def test_get_signing_identity_has_resolution_order(self) -> None: from muse.cli.config import get_signing_identity doc = get_signing_identity.__doc__ or "" assert "Resolution order" in doc or "resolution order" in doc.lower() def test_get_signing_identity_mentions_muse_agent_key(self) -> None: from muse.cli.config import get_signing_identity doc = get_signing_identity.__doc__ or "" assert "MUSE_AGENT_KEY" in doc, ( "get_signing_identity docstring must mention MUSE_AGENT_KEY (legacy step 2)" )