gabriel / muse public
test_migrate_hub_scoping.py python
550 lines 23.4 KB
Raw
sha256:c08228badabf977083c0a945db5b8d1fd5292edab0d0726ebf6c349d1692d528 fix: hub-scope the identity key HD derivation path (musehub#221) Sonnet 5 minor ⚠ breaking 6 days ago
1 """TDD tests for ``muse migrate hub-scoping`` (musehub#221).
2
3 Covers:
4
5 1. Core library (muse.core.hub_scoping_migration)
6 - Pre-Phase-2 path detection (identity domain only)
7 - Old -> new path mapping, hub-scoped
8 - Key re-derivation produces a different (correct) fingerprint per hub
9 - Identity file scanning
10 2. Dry-run plan (no writes, no hub calls)
11 3. Live run (hub called, identity map mutated)
12 4. CLI smoke (muse migrate hub-scoping --dry-run / --no-register)
13 5. Security adversarial inputs and boundary conditions
14
15 Background
16 ----------
17 Prior to Phase 2, the identity key at rotation index 0 was bit-for-bit
18 identical regardless of which hub it was registered with. Phase 2 inserts a
19 hardened ``hub'`` level between ``role'`` and ``index'``. Users with keys
20 derived before Phase 2 must re-derive at the new, hub-scoped path and
21 re-register with the affected hub.
22 """
23
24 from __future__ import annotations
25
26 import json
27 import pathlib
28 from collections.abc import Mapping
29 from unittest.mock import MagicMock
30
31 import pytest
32
33 from muse.core.hdkeys import (
34 DOMAIN_IDENTITY,
35 DOMAIN_CODE,
36 ENTITY_AGENT,
37 ROLE_ATTEST,
38 hub_index,
39 muse_path,
40 )
41 from muse.core.paths import muse_dir
42 from muse.core.slip010 import MUSE_PURPOSE
43
44 FAKE_MNEMONIC = (
45 "abandon abandon abandon abandon abandon abandon "
46 "abandon abandon abandon abandon abandon about"
47 )
48
49 # ---------------------------------------------------------------------------
50 # Helpers
51 # ---------------------------------------------------------------------------
52
53
54 def _pre_scoping_path(entity_type: int = 0, entity_id: int = 0, role: int = 0, index: int = 0) -> str:
55 """Build a pre-Phase-2 six-level identity path (no hub segment)."""
56 return muse_path(DOMAIN_IDENTITY, entity_type, entity_id, role, index)
57
58
59 # ============================================================================
60 # 1. Core: pre-Phase-2 path detection
61 # ============================================================================
62
63
64 class TestIsPreHubScopingHdPath:
65 def test_six_level_identity_path_is_pre_scoping(self) -> None:
66 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
67 assert is_pre_hub_scoping_hd_path(_pre_scoping_path()) is True
68
69 def test_seven_level_hub_scoped_path_is_not_pre_scoping(self) -> None:
70 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
71 hub = hub_index("musehub.ai")
72 scoped = muse_path(DOMAIN_IDENTITY, hub=hub)
73 assert is_pre_hub_scoping_hd_path(scoped) is False
74
75 def test_non_identity_domain_six_level_path_is_not_flagged(self) -> None:
76 """Code/music/etc. domains never had hub scoping — not part of this migration."""
77 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
78 assert is_pre_hub_scoping_hd_path(muse_path(DOMAIN_CODE)) is False
79
80 def test_empty_string_is_not_pre_scoping(self) -> None:
81 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
82 assert is_pre_hub_scoping_hd_path("") is False
83
84 def test_non_muse_purpose_is_not_pre_scoping(self) -> None:
85 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
86 assert is_pre_hub_scoping_hd_path("m/44'/0'/0'/0'/0'/0'") is False
87
88 def test_agent_pre_scoping_path_is_flagged(self) -> None:
89 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
90 assert is_pre_hub_scoping_hd_path(
91 _pre_scoping_path(entity_type=ENTITY_AGENT, entity_id=3)
92 ) is True
93
94
95 # ============================================================================
96 # 2. Core: old -> new path mapping
97 # ============================================================================
98
99
100 class TestNewPathForPreHubScoping:
101 def test_inserts_hub_segment_before_index(self) -> None:
102 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
103 old = _pre_scoping_path()
104 new = new_path_for_pre_hub_scoping(old, "musehub.ai")
105 expected_hub = hub_index("musehub.ai")
106 assert new == muse_path(DOMAIN_IDENTITY, hub=expected_hub)
107
108 def test_different_hubs_produce_different_paths(self) -> None:
109 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
110 old = _pre_scoping_path()
111 a = new_path_for_pre_hub_scoping(old, "musehub.ai")
112 b = new_path_for_pre_hub_scoping(old, "staging.musehub.ai")
113 assert a != b
114
115 def test_preserves_entity_type_and_id(self) -> None:
116 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
117 old = _pre_scoping_path(entity_type=ENTITY_AGENT, entity_id=5)
118 new = new_path_for_pre_hub_scoping(old, "musehub.ai")
119 parts = new.split("/")
120 assert parts[3] == f"{ENTITY_AGENT}'"
121 assert parts[4] == "5'"
122
123 def test_preserves_role_and_index(self) -> None:
124 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
125 old = _pre_scoping_path(role=ROLE_ATTEST, index=2)
126 new = new_path_for_pre_hub_scoping(old, "musehub.ai")
127 parts = new.split("/")
128 assert parts[5] == f"{ROLE_ATTEST}'"
129 assert parts[-1] == "2'"
130
131 def test_non_identity_domain_raises(self) -> None:
132 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
133 with pytest.raises(ValueError, match="Not an identity-domain path"):
134 new_path_for_pre_hub_scoping(muse_path(DOMAIN_CODE), "musehub.ai")
135
136 def test_already_hub_scoped_path_raises(self) -> None:
137 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
138 scoped = muse_path(DOMAIN_IDENTITY, hub=hub_index("musehub.ai"))
139 with pytest.raises(ValueError):
140 new_path_for_pre_hub_scoping(scoped, "musehub.ai")
141
142 def test_output_has_seven_hardened_segments(self) -> None:
143 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
144 new = new_path_for_pre_hub_scoping(_pre_scoping_path(), "musehub.ai")
145 assert new.startswith(f"m/{MUSE_PURPOSE}'")
146 parts = new.split("/")[1:]
147 assert len(parts) == 7
148 assert all(p.endswith("'") for p in parts)
149
150
151 # ============================================================================
152 # 3. Core: key re-derivation
153 # ============================================================================
154
155
156 class TestDeriveFingerprintAtHubScopedPath:
157 def test_old_and_new_fingerprints_differ(self) -> None:
158 from muse.core.hub_scoping_migration import (
159 derive_fingerprint_at_hub_scoped_path,
160 new_path_for_pre_hub_scoping,
161 )
162 from muse.core.domain_migration import derive_fingerprint_at_path
163 from muse.core.bip39 import mnemonic_to_seed
164
165 seed = mnemonic_to_seed(FAKE_MNEMONIC)
166 old_fp = derive_fingerprint_at_path(seed, _pre_scoping_path())
167 new_path = new_path_for_pre_hub_scoping(_pre_scoping_path(), "musehub.ai")
168 new_fp = derive_fingerprint_at_hub_scoped_path(seed, new_path)
169 assert old_fp != new_fp
170
171 def test_different_hubs_produce_different_fingerprints(self) -> None:
172 from muse.core.hub_scoping_migration import (
173 derive_fingerprint_at_hub_scoped_path,
174 new_path_for_pre_hub_scoping,
175 )
176 from muse.core.bip39 import mnemonic_to_seed
177
178 seed = mnemonic_to_seed(FAKE_MNEMONIC)
179 path_a = new_path_for_pre_hub_scoping(_pre_scoping_path(), "musehub.ai")
180 path_b = new_path_for_pre_hub_scoping(_pre_scoping_path(), "staging.musehub.ai")
181 fp_a = derive_fingerprint_at_hub_scoped_path(seed, path_a)
182 fp_b = derive_fingerprint_at_hub_scoped_path(seed, path_b)
183 assert fp_a != fp_b
184
185 def test_deterministic(self) -> None:
186 from muse.core.hub_scoping_migration import (
187 derive_fingerprint_at_hub_scoped_path,
188 new_path_for_pre_hub_scoping,
189 )
190 from muse.core.bip39 import mnemonic_to_seed
191
192 seed = mnemonic_to_seed(FAKE_MNEMONIC)
193 path = new_path_for_pre_hub_scoping(_pre_scoping_path(), "musehub.ai")
194 fp1 = derive_fingerprint_at_hub_scoped_path(seed, path)
195 fp2 = derive_fingerprint_at_hub_scoped_path(seed, path)
196 assert fp1 == fp2
197
198 def test_matches_direct_derive_identity_key(self) -> None:
199 from muse.core.hub_scoping_migration import derive_fingerprint_at_hub_scoped_path
200 from muse.core.bip39 import mnemonic_to_seed
201 from muse.core.keypair import derive_hd_public_info
202
203 seed = mnemonic_to_seed(FAKE_MNEMONIC)
204 hub = hub_index("musehub.ai")
205 _, expected_fp = derive_hd_public_info(seed, hub=hub)
206 actual_fp = derive_fingerprint_at_hub_scoped_path(seed, muse_path(DOMAIN_IDENTITY, hub=hub))
207 assert actual_fp == expected_fp
208
209 def test_fingerprint_is_sha256_prefixed(self) -> None:
210 from muse.core.hub_scoping_migration import (
211 derive_fingerprint_at_hub_scoped_path,
212 new_path_for_pre_hub_scoping,
213 )
214 from muse.core.bip39 import mnemonic_to_seed
215
216 seed = mnemonic_to_seed(FAKE_MNEMONIC)
217 path = new_path_for_pre_hub_scoping(_pre_scoping_path(), "musehub.ai")
218 fp = derive_fingerprint_at_hub_scoped_path(seed, path)
219 assert fp.startswith("sha256:")
220 assert len(fp) == 71
221
222 def test_rejects_six_level_path(self) -> None:
223 from muse.core.hub_scoping_migration import derive_fingerprint_at_hub_scoped_path
224 from muse.core.bip39 import mnemonic_to_seed
225 seed = mnemonic_to_seed(FAKE_MNEMONIC)
226 with pytest.raises(ValueError, match="Cannot parse"):
227 derive_fingerprint_at_hub_scoped_path(seed, _pre_scoping_path())
228
229
230 # ============================================================================
231 # 4. Core: scanning identity map
232 # ============================================================================
233
234
235 class TestScanForPreHubScoping:
236 def test_finds_pre_scoping_entry(self) -> None:
237 from muse.core.hub_scoping_migration import scan_for_pre_hub_scoping
238 identity_map = {
239 "musehub.ai": {"type": "human", "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64},
240 }
241 assert "musehub.ai" in scan_for_pre_hub_scoping(identity_map)
242
243 def test_ignores_already_scoped_entry(self) -> None:
244 from muse.core.hub_scoping_migration import scan_for_pre_hub_scoping
245 identity_map = {
246 "musehub.ai": {
247 "type": "human",
248 "hd_path": muse_path(DOMAIN_IDENTITY, hub=hub_index("musehub.ai")),
249 "fingerprint": "b" * 64,
250 },
251 }
252 assert scan_for_pre_hub_scoping(identity_map) == []
253
254 def test_ignores_non_identity_domain_entry(self) -> None:
255 from muse.core.hub_scoping_migration import scan_for_pre_hub_scoping
256 identity_map = {"musehub.ai": {"hd_path": muse_path(DOMAIN_CODE), "fingerprint": "c" * 64}}
257 assert scan_for_pre_hub_scoping(identity_map) == []
258
259 def test_finds_multiple_hubs(self) -> None:
260 from muse.core.hub_scoping_migration import scan_for_pre_hub_scoping
261 identity_map = {
262 "musehub.ai": {"hd_path": _pre_scoping_path(), "fingerprint": "a" * 64},
263 "staging.musehub.ai": {"hd_path": _pre_scoping_path(), "fingerprint": "b" * 64},
264 }
265 assert set(scan_for_pre_hub_scoping(identity_map)) == {"musehub.ai", "staging.musehub.ai"}
266
267 def test_empty_map_returns_empty(self) -> None:
268 from muse.core.hub_scoping_migration import scan_for_pre_hub_scoping
269 assert scan_for_pre_hub_scoping({}) == []
270
271
272 # ============================================================================
273 # 5. Dry-run and live run
274 # ============================================================================
275
276
277 class TestDryRun:
278 def test_dry_run_returns_plans_without_registering(self) -> None:
279 from muse.core.hub_scoping_migration import run_migration
280 from muse.core.bip39 import mnemonic_to_seed
281
282 identity_map = {
283 "musehub.ai": {
284 "type": "human", "handle": "gabriel",
285 "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64, "algorithm": "ed25519",
286 }
287 }
288 seed = mnemonic_to_seed(FAKE_MNEMONIC)
289 hub_register = MagicMock()
290
291 result = run_migration(identity_map=identity_map, seed=seed, hub_register_fn=hub_register, dry_run=True)
292
293 hub_register.assert_not_called()
294 assert len(result) == 1
295 assert result[0].hub_registered is False
296 assert result[0].new_fingerprint != result[0].old_fingerprint
297
298 def test_dry_run_does_not_mutate_identity_map(self) -> None:
299 from muse.core.hub_scoping_migration import run_migration
300 from muse.core.bip39 import mnemonic_to_seed
301
302 identity_map = {
303 "musehub.ai": {"type": "human", "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64},
304 }
305 seed = mnemonic_to_seed(FAKE_MNEMONIC)
306 run_migration(identity_map=identity_map, seed=seed, hub_register_fn=MagicMock(), dry_run=True)
307 assert identity_map["musehub.ai"]["hd_path"] == _pre_scoping_path()
308 assert identity_map["musehub.ai"]["fingerprint"] == "a" * 64
309
310
311 class TestLiveMigration:
312 def test_live_run_calls_hub_register_and_updates_map(self) -> None:
313 from muse.core.hub_scoping_migration import run_migration
314 from muse.core.bip39 import mnemonic_to_seed
315
316 identity_map = {
317 "musehub.ai": {
318 "type": "human", "handle": "gabriel",
319 "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64, "algorithm": "ed25519",
320 }
321 }
322 seed = mnemonic_to_seed(FAKE_MNEMONIC)
323 hub_register = MagicMock(return_value=True)
324
325 results = run_migration(identity_map=identity_map, seed=seed, hub_register_fn=hub_register, dry_run=False)
326
327 hub_register.assert_called_once()
328 assert results[0].hub_registered is True
329 assert identity_map["musehub.ai"]["hd_path"] == results[0].new_hd_path
330 assert identity_map["musehub.ai"]["fingerprint"] == results[0].new_fingerprint
331
332 def test_live_run_skips_already_scoped_entries(self) -> None:
333 from muse.core.hub_scoping_migration import run_migration
334 from muse.core.bip39 import mnemonic_to_seed
335
336 identity_map = {
337 "musehub.ai": {
338 "type": "human",
339 "hd_path": muse_path(DOMAIN_IDENTITY, hub=hub_index("musehub.ai")),
340 "fingerprint": "b" * 64,
341 }
342 }
343 seed = mnemonic_to_seed(FAKE_MNEMONIC)
344 hub_register = MagicMock()
345 result = run_migration(identity_map=identity_map, seed=seed, hub_register_fn=hub_register, dry_run=False)
346 hub_register.assert_not_called()
347 assert result == []
348
349 def test_partial_failure_updates_successful_entries(self) -> None:
350 from muse.core.hub_scoping_migration import run_migration
351 from muse.core.bip39 import mnemonic_to_seed
352
353 identity_map = {
354 "ok.musehub.ai": {
355 "type": "human", "handle": "gabriel",
356 "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64, "algorithm": "ed25519",
357 },
358 "bad.musehub.ai": {
359 "type": "human", "handle": "gabriel",
360 "hd_path": _pre_scoping_path(), "fingerprint": "b" * 64, "algorithm": "ed25519",
361 },
362 }
363 seed = mnemonic_to_seed(FAKE_MNEMONIC)
364
365 def _flaky_register(hub_key: str, new_fingerprint: str, new_hd_path: str, entry: Mapping[str, object]) -> bool:
366 if "bad" in hub_key:
367 raise RuntimeError("network error")
368 return True
369
370 results = run_migration(identity_map=identity_map, seed=seed, hub_register_fn=_flaky_register, dry_run=False)
371
372 ok_result = next(r for r in results if r.hub_key == "ok.musehub.ai")
373 bad_result = next(r for r in results if r.hub_key == "bad.musehub.ai")
374 assert ok_result.hub_registered is True
375 assert bad_result.hub_registered is False
376 # Both entries still get their hd_path/fingerprint updated.
377 assert identity_map["ok.musehub.ai"]["hd_path"] != _pre_scoping_path()
378 assert identity_map["bad.musehub.ai"]["hd_path"] != _pre_scoping_path()
379
380 def test_multi_hub_migrates_all_with_distinct_fingerprints(self) -> None:
381 from muse.core.hub_scoping_migration import run_migration
382 from muse.core.bip39 import mnemonic_to_seed
383
384 identity_map = {
385 "musehub.ai": {
386 "type": "human", "handle": "gabriel",
387 "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64, "algorithm": "ed25519",
388 },
389 "staging.musehub.ai": {
390 "type": "human", "handle": "gabriel",
391 "hd_path": _pre_scoping_path(), "fingerprint": "a" * 64, "algorithm": "ed25519",
392 },
393 }
394 seed = mnemonic_to_seed(FAKE_MNEMONIC)
395 results = run_migration(
396 identity_map=identity_map, seed=seed, hub_register_fn=MagicMock(return_value=True), dry_run=False
397 )
398 assert len(results) == 2
399 fps = {r.new_fingerprint for r in results}
400 assert len(fps) == 2, "each hub must get a distinct migrated fingerprint"
401
402
403 # ============================================================================
404 # 6. CLI smoke
405 # ============================================================================
406
407
408 def _write_identity_toml(path: pathlib.Path, data: Mapping[str, Mapping[str, object]]) -> None:
409 lines = []
410 for section, fields in data.items():
411 lines.append(f'["{section}"]')
412 for k, v in fields.items():
413 lines.append(f'{k} = "{v}"')
414 lines.append("")
415 path.write_text("\n".join(lines), encoding="utf-8")
416 path.chmod(0o600)
417
418
419 class TestCliDryRun:
420 def test_cli_dry_run_exits_0_with_json(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
421 dot_muse = muse_dir(tmp_path)
422 dot_muse.mkdir()
423 identity_file = dot_muse / "identity.toml"
424 _write_identity_toml(identity_file, {
425 "musehub.ai": {
426 "type": "human", "handle": "gabriel", "algorithm": "ed25519",
427 "fingerprint": "a" * 64, "hd_path": _pre_scoping_path(),
428 }
429 })
430
431 import muse.core.identity as id_module
432 monkeypatch.setattr(id_module, "_IDENTITY_DIR", dot_muse)
433 monkeypatch.setattr(id_module, "_IDENTITY_FILE", identity_file)
434
435 import muse.core.keychain as kc_module
436 monkeypatch.setattr(kc_module, "load", lambda: FAKE_MNEMONIC)
437
438 from tests.cli_test_helper import CliRunner
439 runner = CliRunner()
440 result = runner.invoke(None, ["migrate", "hub-scoping", "--dry-run", "--json"])
441
442 assert result.exit_code == 0, result.output
443 data = json.loads(result.output)
444 assert data["dry_run"] is True
445 assert data["entries_found"] == 1
446 assert data["entries_migrated"] == 0
447
448 def test_cli_no_pre_scoping_entries_exits_0(
449 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
450 ) -> None:
451 dot_muse = muse_dir(tmp_path)
452 dot_muse.mkdir()
453 identity_file = dot_muse / "identity.toml"
454 _write_identity_toml(identity_file, {
455 "musehub.ai": {
456 "type": "human", "handle": "gabriel", "algorithm": "ed25519",
457 "fingerprint": "b" * 64,
458 "hd_path": muse_path(DOMAIN_IDENTITY, hub=hub_index("musehub.ai")),
459 }
460 })
461
462 import muse.core.identity as id_module
463 monkeypatch.setattr(id_module, "_IDENTITY_DIR", dot_muse)
464 monkeypatch.setattr(id_module, "_IDENTITY_FILE", identity_file)
465
466 import muse.core.keychain as kc_module
467 monkeypatch.setattr(kc_module, "load", lambda: FAKE_MNEMONIC)
468
469 from tests.cli_test_helper import CliRunner
470 runner = CliRunner()
471 result = runner.invoke(None, ["migrate", "hub-scoping", "--dry-run", "--json"])
472
473 assert result.exit_code == 0, result.output
474 data = json.loads(result.output)
475 assert data["entries_found"] == 0
476
477 def test_cli_no_register_persists_to_identity_toml(
478 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
479 ) -> None:
480 dot_muse = muse_dir(tmp_path)
481 dot_muse.mkdir()
482 identity_file = dot_muse / "identity.toml"
483 _write_identity_toml(identity_file, {
484 "musehub.ai": {
485 "type": "human", "handle": "gabriel", "algorithm": "ed25519",
486 "fingerprint": "a" * 64, "hd_path": _pre_scoping_path(),
487 }
488 })
489
490 import muse.core.identity as id_module
491 monkeypatch.setattr(id_module, "_IDENTITY_DIR", dot_muse)
492 monkeypatch.setattr(id_module, "_IDENTITY_FILE", identity_file)
493
494 import muse.core.keychain as kc_module
495 monkeypatch.setattr(kc_module, "load", lambda: FAKE_MNEMONIC)
496
497 from tests.cli_test_helper import CliRunner
498 runner = CliRunner()
499 result = runner.invoke(None, ["migrate", "hub-scoping", "--no-register", "--json"])
500 assert result.exit_code == 0, result.output
501
502 import tomllib
503 data = tomllib.loads(identity_file.read_text())
504 assert data["musehub.ai"]["hd_path"] != _pre_scoping_path()
505 assert "/" + str(hub_index("musehub.ai")) + "'" in data["musehub.ai"]["hd_path"]
506
507
508 # ============================================================================
509 # 7. Security: adversarial inputs
510 # ============================================================================
511
512
513 class TestSecurity:
514 def test_malformed_path_missing_hardened_marker_not_pre_scoping(self) -> None:
515 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
516 assert is_pre_hub_scoping_hd_path(f"m/{MUSE_PURPOSE}/{DOMAIN_IDENTITY}/0/0/0/0") is False
517
518 def test_path_with_too_few_segments_not_pre_scoping(self) -> None:
519 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
520 assert is_pre_hub_scoping_hd_path(f"m/{MUSE_PURPOSE}'/{DOMAIN_IDENTITY}'/0'") is False
521
522 def test_path_with_extra_segments_not_pre_scoping(self) -> None:
523 """A path with 8+ segments is never mistaken for a pre-Phase-2 six-level path."""
524 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
525 assert is_pre_hub_scoping_hd_path(
526 f"m/{MUSE_PURPOSE}'/{DOMAIN_IDENTITY}'/0'/0'/0'/0'/0'"
527 ) is False
528
529 def test_empty_string_not_pre_scoping(self) -> None:
530 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
531 assert is_pre_hub_scoping_hd_path("") is False
532
533 def test_whitespace_only_not_pre_scoping(self) -> None:
534 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
535 assert is_pre_hub_scoping_hd_path(" ") is False
536
537 def test_path_traversal_attempt_not_pre_scoping(self) -> None:
538 from muse.core.hub_scoping_migration import is_pre_hub_scoping_hd_path
539 assert is_pre_hub_scoping_hd_path("m/1075233755'/../../../etc/passwd") is False
540
541 def test_new_path_hub_key_hashed_not_interpolated_raw(self) -> None:
542 """hub_key text never leaks verbatim into the derived path — only its hash does."""
543 from muse.core.hub_scoping_migration import new_path_for_pre_hub_scoping
544 malicious_hub = "musehub.ai/../../etc/passwd"
545 new = new_path_for_pre_hub_scoping(_pre_scoping_path(), malicious_hub)
546 assert "etc" not in new
547 assert "passwd" not in new
548 parts = new.split("/")[1:]
549 assert len(parts) == 7
550 assert all(p.endswith("'") and p[:-1].isdigit() for p in parts)
File History 1 commit
sha256:c08228badabf977083c0a945db5b8d1fd5292edab0d0726ebf6c349d1692d528 fix: hub-scope the identity key HD derivation path (musehub#221) Sonnet 5 minor ⚠ 6 days ago