gabriel / muse public
path_cmd.py python
213 lines 6.7 KB
Raw
sha256:2a703f78341332ef0beb9856d2267de6aec89b3883c31519b6900b667d026e62 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor ⚠ breaking 8 days ago
1 """muse path — HD derivation path tooling.
2
3 Subcommands::
4
5 muse path annotate <path> Decode a raw HD path to human-readable form.
6
7 Example::
8
9 $ muse path annotate "m/1075233755'/1660078172'/0'/0'/0'/0'"
10 purpose: muse (1075233755)
11 domain: muse/identity (1660078172)
12 entity_type: human (0)
13 entity_id: 0
14 role: sign (0)
15 index: 0
16
17 JSON schema for ``muse path annotate --json``::
18
19 {
20 "raw_path": "m/1075233755'/1660078172'/0'/0'/0'/0'",
21 "purpose": "muse",
22 "purpose_idx": 1075233755,
23 "domain": "muse/identity",
24 "domain_idx": 1660078172,
25 "domain_registered": true,
26 "entity_type": "human",
27 "entity_type_idx": 0,
28 "entity_id": 0,
29 "role": "sign",
30 "role_idx": 0,
31 "index": 0
32 }
33
34 Unrecognised integers are rendered as their raw value with a ``?`` suffix.
35 """
36
37 import argparse
38 import json
39 import re
40 import sys
41 from typing import TypedDict
42
43 from muse.core.envelope import EnvelopeJson, make_envelope
44 from muse.core.errors import ExitCode
45 from muse.core.paths import user_domain_registry_path
46 from muse.core.slip010 import MUSE_PURPOSE
47 from muse.core.timing import start_timer
48 from muse.core.types import load_json_file
49
50 # Inline the lookup to avoid circular imports — domain_cmd is not a lib.
51 import hashlib
52 import os
53 import pathlib
54
55 class _DomainSeed(TypedDict):
56 name: str
57 index: int
58
59
60 # ---------------------------------------------------------------------------
61 # Known constant maps
62 # ---------------------------------------------------------------------------
63
64 _ENTITY_NAMES: dict[int, str] = {
65 0: "human",
66 1: "agent",
67 2: "org",
68 }
69
70 _ROLE_NAMES: dict[int, str] = {
71 0: "sign",
72 1: "receive",
73 2: "provision",
74 3: "attest",
75 4: "delegate",
76 }
77
78 _SEED_DOMAINS: list[_DomainSeed] = [
79 {"name": "muse/identity", "index": 1660078172},
80 {"name": "muse/payments", "index": 284229149},
81 {"name": "muse/code", "index": 678195575},
82 {"name": "muse/music", "index": 1755707987},
83 {"name": "muse/midi", "index": 1444628350},
84 {"name": "muse/prose", "index": 1658731548},
85 {"name": "muse/blockchain", "index": 1556829714},
86 {"name": "muse/generic", "index": 2023564266},
87 ]
88
89 def _load_domain_map() -> dict[int, str]:
90 """Return index → name map from the registry, falling back to the seed."""
91 paths = [
92 os.environ.get("MUSE_DOMAIN_REGISTRY", ""),
93 str(user_domain_registry_path()),
94 ]
95 for p in paths:
96 if p:
97 data = load_json_file(pathlib.Path(p))
98 if isinstance(data, dict):
99 return {e["index"]: e["name"] for e in data.get("domains", [])}
100 return {e["index"]: e["name"] for e in _SEED_DOMAINS}
101
102 # ---------------------------------------------------------------------------
103 # Path parser
104 # ---------------------------------------------------------------------------
105
106 _PATH_RE = re.compile(
107 r"^m"
108 r"/(\d+)'?" # purpose
109 r"/(\d+)'?" # domain
110 r"/(\d+)'?" # entity_type
111 r"/(\d+)'?" # entity_id
112 r"/(\d+)'?" # role
113 r"/(\d+)'?$" # index
114 )
115
116 class _PathAnnotation(TypedDict):
117 raw_path: str
118 purpose: str
119 purpose_idx: int
120 domain: str
121 domain_idx: int
122 domain_registered: bool
123 entity_type: str
124 entity_type_idx: int
125 entity_id: int
126 role: str
127 role_idx: int
128 index: int
129
130
131 def _annotate(raw: str) -> _PathAnnotation:
132 m = _PATH_RE.match(raw.strip())
133 if not m:
134 raise ValueError(
135 f"Cannot parse path {raw!r}. "
136 "Expected format: m/<purpose>'/<domain>'/<entity_type>'/<entity_id>'/<role>'/<index>'"
137 )
138 purpose_idx, domain_idx, et_idx, entity_id, role_idx, index = (
139 int(m.group(i)) for i in range(1, 7)
140 )
141
142 domain_map = _load_domain_map()
143
144 purpose_name = "muse" if purpose_idx == MUSE_PURPOSE else f"{purpose_idx}?"
145 domain_name = domain_map.get(domain_idx, f"{domain_idx}?")
146 domain_registered = domain_idx in domain_map
147 entity_name = _ENTITY_NAMES.get(et_idx, f"{et_idx}?")
148 role_name = _ROLE_NAMES.get(role_idx, f"{role_idx}?")
149
150 return {
151 "raw_path": raw.strip(),
152 "purpose": purpose_name,
153 "purpose_idx": purpose_idx,
154 "domain": domain_name,
155 "domain_idx": domain_idx,
156 "domain_registered": domain_registered,
157 "entity_type": entity_name,
158 "entity_type_idx": et_idx,
159 "entity_id": entity_id,
160 "role": role_name,
161 "role_idx": role_idx,
162 "index": index,
163 }
164
165 # ---------------------------------------------------------------------------
166 # Subcommand: annotate
167 # ---------------------------------------------------------------------------
168
169 def _run_annotate(args: argparse.Namespace) -> None:
170 elapsed = start_timer()
171 try:
172 result = _annotate(args.path)
173 except ValueError as exc:
174 print(json.dumps({"error": str(exc)}), file=sys.stderr)
175 raise SystemExit(ExitCode.USER_ERROR)
176
177 if args.json_out:
178 print(json.dumps({**make_envelope(elapsed), **result}))
179 else:
180 print(f"purpose: {result['purpose']} ({result['purpose_idx']})")
181 reg = "" if result["domain_registered"] else " [unregistered]"
182 print(f"domain: {result['domain']} ({result['domain_idx']}){reg}")
183 print(f"entity_type: {result['entity_type']} ({result['entity_type_idx']})")
184 print(f"entity_id: {result['entity_id']}")
185 print(f"role: {result['role']} ({result['role_idx']})")
186 print(f"index: {result['index']}")
187
188 # ---------------------------------------------------------------------------
189 # Registration
190 # ---------------------------------------------------------------------------
191
192 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
193 """Register the ``muse path`` namespace."""
194 parser = subparsers.add_parser(
195 "path",
196 help="HD derivation path tooling — annotate raw paths to human-readable form.",
197 description=__doc__,
198 formatter_class=argparse.RawDescriptionHelpFormatter,
199 )
200 path_subs = parser.add_subparsers(dest="path_subcmd", metavar="SUBCMD")
201 path_subs.required = True
202
203 # ── annotate ───────────────────────────────────────────────────────
204 p_annotate = path_subs.add_parser(
205 "annotate",
206 help="Decode a raw HD path to human-readable form.",
207 )
208 p_annotate.add_argument(
209 "path",
210 help="Raw HD path, e.g. \"m/1075233755'/1660078172'/0'/0'/0'/0'\".",
211 )
212 p_annotate.add_argument("--json", "-j", action="store_true", dest="json_out")
213 p_annotate.set_defaults(func=_run_annotate)
File History 1 commit
sha256:2a703f78341332ef0beb9856d2267de6aec89b3883c31519b6900b667d026e62 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor 8 days ago