gabriel / muse public
fix BREAKING task/221-hub-scoped-hd-derivation #2 / 2
AI Agent gabriel · 5 days ago · Sep 18, 2026 · Diff

fix: hub-scope the identity key HD derivation path (musehub#221)

Prior to this fix, muse_path()/derive_identity_key() had no per-hub level in the HD path -- the identity key at rotation index 0 was bit-for-bit identical regardless of which hub it was registered with. Confirmed live against a real ~/.muse/identity.toml: localhost:1337 and musehub.ai had identical fingerprints and hd_path, both at index 0.

- hdkeys.py: new hub_index(canonical_hostname) function (same sha256-hash-and-mask pattern as domain_index/agent_id_to_slot). muse_path()/derive_key()/derive_identity_key() gain an optional hub parameter -- None reproduces the original six-level path byte-for-byte (every non-identity domain is unaffected); a real hub index inserts a seventh hardened level between role' and index'. - keypair.py: derive_hd_public_info() threads the new hub parameter through to derive_identity_key(). - auth.py: muse auth keygen/recover/rotate now compute and pass hub=hub_index(hostname) for every real identity-key derivation. rotate detects whether the EXISTING key is pre-#221 (six-level) or already hub-scoped (seven-level) and re-derives the old key under whichever scheme it actually was registered with, so the challenge-response deregistration step still signs with the right key -- only the NEW post-rotation key is always hub-scoped going forward. security-check's fingerprint-verification does the same legacy-vs-scoped detection. - path_cmd.py: muse path annotate now parses both six-level and seven-level (hub-scoped) paths and reports hub_idx/hub_scoped in its output. - hub_scoping_migration.py (new) + migrate_cmd.py: muse migrate hub-scoping [--dry-run] [--no-register] detects six-level identity-domain entries in identity.toml, re-derives each at a hub-scoped path, and re-registers with that specific hub. Mirrors the existing domain-integers migration exactly. - app.py: registered migrate_cmd.register() -- this was never wired up before (a separate, pre-existing latent bug found in the course of this fix; muse migrate domain-integers was unreachable via the real CLI despite being fully implemented and documented). - Updated 5 existing tests whose fixtures assumed the old bug as correct behavior (most notably test_10_recoveries_different_hubs, which explicitly asserted "same mnemonic -> same fingerprint regardless of hub" -- that assertion was the vulnerability). - Added 52 new tests (test_migrate_hub_scoping.py, test_path_annotate.py) covering path detection, mapping, key re-derivation, scanning, dry-run/live migration, CLI smoke, and adversarial inputs.

1165 tests across every affected area (auth, hdkeys, migrate, agent signing, keychain, security-zeroing, path annotate) pass.

sha256:c08228badabf977083c0a945db5b8d1fd5292edab0d0726ebf6c349d1692d528 sha
+116 ~162 symbols
14 changed · 1194 in snapshot files
sha256:aa073c2d8469cabd579a7da002afe0b74ac168102ad3032d5da3472ea630262f snapshot
+116
symbols added
~162
symbols modified
14
files changed
1194
files in snapshot
0
dead code introduced
Semantic Changes 278 symbols
~ muse/core/hub_scoping_migration.py .py 21 symbols added
+ MigrationPlan class class MigrationPlan L174–179
+ MigrationResult class class MigrationResult L182–190
+ _SEVEN_LEVEL_RE variable variable _SEVEN_LEVEL_RE L113–121
+ _SIX_LEVEL_RE variable variable _SIX_LEVEL_RE L50–57
+ _parse_six_level function function _parse_six_level L59–64
+ build_plans function function build_plans L192–203
+ derive_fingerprint_at_hub_scoped_path function function derive_fingerprint_at_hub_scoped_path L123–151
+ DOMAIN_IDENTITY import import DOMAIN_IDENTITY L39–39
+ MUSE_PURPOSE import import MUSE_PURPOSE L41–41
+ Mapping import import Mapping L36–36
+ dataclass class import dataclass L37–37
+ hostname_from_url import import hostname_from_url L40–40
+ hub_index import import hub_index L39–39
+ logging import import logging L34–34
+ muse_path import import muse_path L39–39
+ re import import re L35–35
+ is_pre_hub_scoping_hd_path function function is_pre_hub_scoping_hd_path L70–82
+ logger variable variable logger L43–43
+ new_path_for_pre_hub_scoping function function new_path_for_pre_hub_scoping L84–110
+ run_migration function function run_migration L209–274
+ scan_for_pre_hub_scoping function function scan_for_pre_hub_scoping L157–167
~ tests/test_migrate_hub_scoping.py .py 65 symbols added
+ FAKE_MNEMONIC variable variable FAKE_MNEMONIC L44–47
+ TestCliDryRun class class TestCliDryRun L419–505
+ test_cli_dry_run_exits_0_with_json method method test_cli_dry_run_exits_0_with_json L420–446
+ test_cli_no_pre_scoping_entries_exits_0 method method test_cli_no_pre_scoping_entries_exits_0 L448–475
+ test_cli_no_register_persists_to_identity_toml method method test_cli_no_register_persists_to_identity_toml L477–505
+ TestDeriveFingerprintAtHubScopedPath class class TestDeriveFingerprintAtHubScopedPath L156–227
+ test_deterministic method method test_deterministic L185–196
+ test_different_hubs_produce_different_fingerprints method method test_different_hubs_produce_different_fingerprints L171–183
+ test_fingerprint_is_sha256_prefixed method method test_fingerprint_is_sha256_prefixed L209–220
+ test_matches_direct_derive_identity_key method method test_matches_direct_derive_identity_key L198–207
+ test_old_and_new_fingerprints_differ method method test_old_and_new_fingerprints_differ L157–169
+ test_rejects_six_level_path method method test_rejects_six_level_path L222–227
+ TestDryRun class class TestDryRun L277–308
+ test_dry_run_does_not_mutate_identity_map method method test_dry_run_does_not_mutate_identity_map L298–308
+ test_dry_run_returns_plans_without_registering method method test_dry_run_returns_plans_without_registering L278–296
+ TestIsPreHubScopingHdPath class class TestIsPreHubScopingHdPath L64–92
+ test_agent_pre_scoping_path_is_flagged method method test_agent_pre_scoping_path_is_flagged L88–92
+ test_empty_string_is_not_pre_scoping method method test_empty_string_is_not_pre_scoping L80–82
+ test_non_identity_domain_six_level_path_is_not_flagged method method test_non_identity_domain_six_level_path_is_not_flagged L75–78
+ test_non_muse_purpose_is_not_pre_scoping method method test_non_muse_purpose_is_not_pre_scoping L84–86
+ test_seven_level_hub_scoped_path_is_not_pre_scoping method method test_seven_level_hub_scoped_path_is_not_pre_scoping L69–73
+ test_six_level_identity_path_is_pre_scoping method method test_six_level_identity_path_is_pre_scoping L65–67
+ TestLiveMigration class class TestLiveMigration L311–400
+ test_live_run_calls_hub_register_and_updates_map method method test_live_run_calls_hub_register_and_updates_map L312–330
+ test_live_run_skips_already_scoped_entries method method test_live_run_skips_already_scoped_entries L332–347
+ test_multi_hub_migrates_all_with_distinct_fingerprints method method test_multi_hub_migrates_all_with_distinct_fingerprints L380–400
+ test_partial_failure_updates_successful_entries method method test_partial_failure_updates_successful_entries L349–378
+ TestNewPathForPreHubScoping class class TestNewPathForPreHubScoping L100–148
+ test_already_hub_scoped_path_raises method method test_already_hub_scoped_path_raises L136–140
+ test_different_hubs_produce_different_paths method method test_different_hubs_produce_different_paths L108–113
+ test_inserts_hub_segment_before_index method method test_inserts_hub_segment_before_index L101–106
+ test_non_identity_domain_raises method method test_non_identity_domain_raises L131–134
+ test_output_has_seven_hardened_segments method method test_output_has_seven_hardened_segments L142–148
+ test_preserves_entity_type_and_id method method test_preserves_entity_type_and_id L115–121
+ test_preserves_role_and_index method method test_preserves_role_and_index L123–129
+ TestScanForPreHubScoping class class TestScanForPreHubScoping L235–269
+ test_empty_map_returns_empty method method test_empty_map_returns_empty L267–269
+ test_finds_multiple_hubs method method test_finds_multiple_hubs L259–265
+ test_finds_pre_scoping_entry method method test_finds_pre_scoping_entry L236–241
+ test_ignores_already_scoped_entry method method test_ignores_already_scoped_entry L243–252
+ test_ignores_non_identity_domain_entry method method test_ignores_non_identity_domain_entry L254–257
+ TestSecurity class class TestSecurity L513–550
+ test_empty_string_not_pre_scoping method method test_empty_string_not_pre_scoping L529–531
+ test_malformed_path_missing_hardened_marker_not_pre_scoping method method test_malformed_path_missing_hardened_marker_not_pre_scoping L514–516
+ test_new_path_hub_key_hashed_not_interpolated_raw method method test_new_path_hub_key_hashed_not_interpolated_raw L541–550
+ test_path_traversal_attempt_not_pre_scoping method method test_path_traversal_attempt_not_pre_scoping L537–539
+ test_path_with_extra_segments_not_pre_scoping method method test_path_with_extra_segments_not_pre_scoping L522–527
+ test_path_with_too_few_segments_not_pre_scoping method method test_path_with_too_few_segments_not_pre_scoping L518–520
+ test_whitespace_only_not_pre_scoping method method test_whitespace_only_not_pre_scoping L533–535
+ _pre_scoping_path function function _pre_scoping_path L54–56
+ _write_identity_toml function function _write_identity_toml L408–416
+ DOMAIN_CODE import import DOMAIN_CODE L33–33
+ DOMAIN_IDENTITY import import DOMAIN_IDENTITY L33–33
+ ENTITY_AGENT import import ENTITY_AGENT L33–33
+ MUSE_PURPOSE import import MUSE_PURPOSE L42–42
+ MagicMock import import MagicMock L29–29
+ Mapping import import Mapping L28–28
+ ROLE_ATTEST import import ROLE_ATTEST L33–33
+ annotations import import annotations L24–24
+ hub_index import import hub_index L33–33
+ json import import json L26–26
+ muse_dir import import muse_dir L41–41
+ muse_path import import muse_path L33–33
+ pathlib import import pathlib L27–27
+ pytest import import pytest L31–31
~ tests/test_path_annotate.py .py 25 symbols added
+ TestAnnotateCli class class TestAnnotateCli L69–102
+ test_cli_json_seven_level method method test_cli_json_seven_level L79–86
+ test_cli_json_six_level method method test_cli_json_six_level L70–77
+ test_cli_malformed_path_exits_nonzero method method test_cli_malformed_path_exits_nonzero L100–102
+ test_cli_text_output_includes_hub_line_when_scoped method method test_cli_text_output_includes_hub_line_when_scoped L88–93
+ test_cli_text_output_omits_hub_line_when_not_scoped method method test_cli_text_output_omits_hub_line_when_not_scoped L95–98
+ TestAnnotateMalformed class class TestAnnotateMalformed L51–66
+ test_eight_segments_raises method method test_eight_segments_raises L56–58
+ test_empty_raises method method test_empty_raises L64–66
+ test_five_segments_raises method method test_five_segments_raises L52–54
+ test_garbage_raises method method test_garbage_raises L60–62
+ TestAnnotateSevenLevel class class TestAnnotateSevenLevel L32–48
+ test_decodes_hub_scoped_path method method test_decodes_hub_scoped_path L33–38
+ test_preserves_other_fields_when_hub_scoped method method test_preserves_other_fields_when_hub_scoped L40–48
+ TestAnnotateSixLevel class class TestAnnotateSixLevel L20–29
+ test_decodes_pre_hub_scoping_path method method test_decodes_pre_hub_scoping_path L21–29
+ CliRunner import import CliRunner L15–15
+ DOMAIN_IDENTITY import import DOMAIN_IDENTITY L14–14
+ _annotate import import _annotate L13–13
+ annotations import import annotations L7–7
+ hub_index import import hub_index L14–14
+ json import import json L9–9
+ muse_path import import muse_path L14–14
+ pytest import import pytest L11–11
+ runner variable variable runner L17–17
~ muse/cli/commands/migrate_cmd.py .py 1 symbol added, 1 symbol modified
+ _run_hub_scoping function function _run_hub_scoping L262–384
~ muse/cli/commands/path_cmd.py .py 1 symbol added, 4 symbols modified
+ _SEGMENT_RE variable variable _SEGMENT_RE L119–119
~ muse/core/hdkeys.py .py 1 symbol added, 4 symbols modified
+ hub_index function function hub_index L298–330
~ muse/core/keypair.py .py 1 symbol modified
Files Changed
+3 ~11
1194 in snapshot

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:c08228badabf977083c0a945db5b8d1fd5292edab0d0726ebf6c349d1692d528 --body "your comment"