"""Tests for agent-first signing — compound identity keys and key paths. Covers: - identity.py: compound key load/save/clear, provisioned_by field - keypair.py: agent-specific key paths and HD key generation - resolve_signing_identity: agent key → human key fallback """ from __future__ import annotations import pathlib import json import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, ) from muse.core.identity import ( IdentityEntry, _identity_key, clear_identity, hostname_from_url, list_all_identities, load_identity, resolve_signing_identity, save_identity, ) from muse.core.keypair import ( generate_hd_keypair, key_path_for, load_private_key, load_private_key_from_pem, ) # Fixed 64-byte seeds for deterministic test keys _SEED_A = b"\x00" * 64 _SEED_B = b"\x01" * 64 # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def isolated_identity(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Redirect the identity store to a temp directory for test isolation.""" import muse.core.identity as _id_mod identity_dir = tmp_path / ".muse" identity_dir.mkdir() monkeypatch.setattr(_id_mod, "_IDENTITY_DIR", identity_dir) monkeypatch.setattr(_id_mod, "_IDENTITY_FILE", identity_dir / "identity.toml") return identity_dir @pytest.fixture() def isolated_keys(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Redirect key storage to a temp directory.""" import muse.core.keypair as _kp_mod keys_dir = tmp_path / ".muse" / "keys" keys_dir.mkdir(parents=True) monkeypatch.setattr(_kp_mod, "_KEYS_DIR", keys_dir) return keys_dir # --------------------------------------------------------------------------- # _identity_key helper # --------------------------------------------------------------------------- class TestIdentityKey: def test_human_key_is_bare_hostname(self) -> None: assert _identity_key("localhost:10003") == "localhost:10003" def test_agent_key_uses_hash_separator(self) -> None: assert _identity_key("localhost:10003", "agent-abc") == "localhost:10003#agent-abc" def test_none_agent_id_gives_bare_hostname(self) -> None: assert _identity_key("musehub.ai", None) == "musehub.ai" def test_empty_agent_id_gives_bare_hostname(self) -> None: # empty string is falsy assert _identity_key("musehub.ai", "") == "musehub.ai" # --------------------------------------------------------------------------- # load_identity / save_identity compound keys # --------------------------------------------------------------------------- class TestCompoundIdentityKeys: def test_human_and_agent_entries_coexist( self, isolated_identity: pathlib.Path ) -> None: hub = "http://localhost:10003" human: IdentityEntry = { "type": "human", "handle": "gabriel", "key_path": "/fake/human.pem", "algorithm": "ed25519", "fingerprint": "a" * 64, } agent: IdentityEntry = { "type": "agent", "handle": "agentception-abc", "key_path": "/fake/agent.pem", "algorithm": "ed25519", "fingerprint": "b" * 64, "provisioned_by": "gabriel", } save_identity(hub, human) save_identity(hub, agent, agent_id="agentception-abc") loaded_human = load_identity(hub) loaded_agent = load_identity(hub, agent_id="agentception-abc") assert loaded_human is not None assert loaded_human["handle"] == "gabriel" assert loaded_human["type"] == "human" assert loaded_agent is not None assert loaded_agent["handle"] == "agentception-abc" assert loaded_agent["type"] == "agent" assert loaded_agent.get("provisioned_by") == "gabriel" def test_agent_entry_does_not_shadow_human( self, isolated_identity: pathlib.Path ) -> None: hub = "http://localhost:10003" human: IdentityEntry = {"type": "human", "handle": "gabriel"} save_identity(hub, human) # Load without agent_id → human entry loaded = load_identity(hub) assert loaded is not None assert loaded["handle"] == "gabriel" def test_load_missing_agent_returns_none( self, isolated_identity: pathlib.Path ) -> None: hub = "http://localhost:10003" assert load_identity(hub, agent_id="nonexistent") is None def test_clear_agent_identity_leaves_human_intact( self, isolated_identity: pathlib.Path ) -> None: hub = "http://localhost:10003" human: IdentityEntry = {"type": "human", "handle": "gabriel"} agent: IdentityEntry = {"type": "agent", "handle": "agentception-abc", "provisioned_by": "gabriel"} save_identity(hub, human) save_identity(hub, agent, agent_id="agentception-abc") cleared = clear_identity(hub, agent_id="agentception-abc") assert cleared is True assert load_identity(hub) is not None # human still present assert load_identity(hub, agent_id="agentception-abc") is None # agent gone def test_provisioned_by_roundtrip( self, isolated_identity: pathlib.Path ) -> None: """provisioned_by survives a save/load cycle.""" hub = "http://localhost:10003" agent: IdentityEntry = { "type": "agent", "handle": "bot-001", "provisioned_by": "gabriel", "algorithm": "ed25519", "fingerprint": "c" * 64, } save_identity(hub, agent, agent_id="bot-001") loaded = load_identity(hub, agent_id="bot-001") assert loaded is not None assert loaded.get("provisioned_by") == "gabriel" def test_list_all_includes_compound_keys( self, isolated_identity: pathlib.Path ) -> None: hub = "http://localhost:10003" save_identity(hub, {"type": "human", "handle": "gabriel"}) save_identity(hub, {"type": "agent", "handle": "bot"}, agent_id="bot") all_ids = list_all_identities() assert "localhost:10003" in all_ids assert "localhost:10003#bot" in all_ids # --------------------------------------------------------------------------- # keypair agent-specific paths # --------------------------------------------------------------------------- class TestAgentKeyPaths: def test_human_key_path(self) -> None: p = key_path_for("localhost:10003") assert "__" not in p.name assert p.name == "localhost_10003.pem" def test_agent_key_path_uses_double_underscore(self) -> None: p = key_path_for("localhost:10003", "agentception-abc") assert p.name == "localhost_10003__agentception-abc.pem" def test_generate_agent_keypair_creates_distinct_file( self, isolated_keys: pathlib.Path ) -> None: pub_human, fp_human = generate_hd_keypair("localhost:10003", _SEED_A) pub_agent, fp_agent = generate_hd_keypair("localhost:10003", _SEED_B, "agentception-abc") assert fp_human != fp_agent # different seeds → different keys assert (isolated_keys / "localhost_10003.pem").is_file() assert (isolated_keys / "localhost_10003__agentception-abc.pem").is_file() def test_load_private_key_with_agent_id( self, isolated_keys: pathlib.Path ) -> None: generate_hd_keypair("localhost:10003", _SEED_A, "bot-42") key = load_private_key("localhost:10003", "bot-42") assert key is not None assert isinstance(key, Ed25519PrivateKey) def test_load_private_key_agent_id_not_found_returns_none( self, isolated_keys: pathlib.Path ) -> None: # Human key exists but no agent key generate_hd_keypair("localhost:10003", _SEED_A) key = load_private_key("localhost:10003", "nonexistent-agent") assert key is None # --------------------------------------------------------------------------- # resolve_signing_identity — agent key → fallback chain # --------------------------------------------------------------------------- class TestResolveSigningIdentity: def test_agent_key_used_when_registered( self, isolated_identity: pathlib.Path, isolated_keys: pathlib.Path, ) -> None: hub = "http://localhost:10003" hostname = hostname_from_url(hub) agent_id = "agentception-abc" # Generate two distinct keys generate_hd_keypair(hostname, _SEED_A) generate_hd_keypair(hostname, _SEED_B, agent_id) agent_key_path = str(key_path_for(hostname, agent_id)) save_identity( hub, {"type": "agent", "handle": agent_id, "key_path": agent_key_path, "algorithm": "ed25519"}, agent_id=agent_id, ) result = resolve_signing_identity(hub, agent_id=agent_id) assert result is not None handle, private_key = result assert handle == agent_id assert isinstance(private_key, Ed25519PrivateKey) def test_falls_back_to_human_when_no_agent_key( self, isolated_identity: pathlib.Path, isolated_keys: pathlib.Path, ) -> None: hub = "http://localhost:10003" hostname = hostname_from_url(hub) # Only human key registered generate_hd_keypair(hostname, _SEED_A) human_key_path = str(key_path_for(hostname)) save_identity( hub, {"type": "human", "handle": "gabriel", "key_path": human_key_path, "algorithm": "ed25519"}, ) # Ask for agent signing but no agent entry → falls back to human result = resolve_signing_identity(hub, agent_id="unregistered-agent") assert result is not None handle, _ = result assert handle == "gabriel" def test_no_identity_returns_none( self, isolated_identity: pathlib.Path ) -> None: assert resolve_signing_identity("http://localhost:10003") is None def test_human_resolve_without_agent_id( self, isolated_identity: pathlib.Path, isolated_keys: pathlib.Path, ) -> None: hub = "http://localhost:10003" hostname = hostname_from_url(hub) generate_hd_keypair(hostname, _SEED_A) save_identity( hub, { "type": "human", "handle": "gabriel", "key_path": str(key_path_for(hostname)), "algorithm": "ed25519", }, ) result = resolve_signing_identity(hub) assert result is not None handle, _ = result assert handle == "gabriel" # --------------------------------------------------------------------------- # load_private_key_from_pem # --------------------------------------------------------------------------- class TestLoadPrivateKeyFromPem: def _make_pem(self) -> bytes: key = Ed25519PrivateKey.generate() return key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()) def test_valid_pem_returns_key(self) -> None: pem = self._make_pem() key = load_private_key_from_pem(pem) assert isinstance(key, Ed25519PrivateKey) def test_invalid_pem_returns_none(self) -> None: assert load_private_key_from_pem(b"not a pem") is None def test_empty_bytes_returns_none(self) -> None: assert load_private_key_from_pem(b"") is None