Signing identity resolution and proposalType canonical cleanup
Context
MSign authentication uses Ed25519 keys stored in ~/.muse/identity.toml, keyed by hostname. When a caller targets a remote that is on a different host than the repo's default [hub] url (e.g. a named remote pointing to staging.musehub.ai while the repo config says localhost:1337), the signing identity must be resolved against the actual target URL — not the default hub URL.
The canonical call is:
url = _resolve_remote_url(root, remote) # resolve first
token = get_signing_identity(root, remote_url=url) # then look up by hostname
Resolving identity before the URL is known causes a wrong-hostname lookup, producing HTTP 401 even when the user is correctly registered on the target remote. This is exactly what caused muse ls-remote staging to fail.
Audit results — all callers of get_signing_identity and fetch_remote_info
| File | Pattern | Status |
|---|---|---|
ls_remote.py |
Called before URL resolved; now get_signing_identity(root, remote_url=url) post-resolution |
✅ Fixed (feat/test-merge-6) |
fetch.py |
get_signing_identity(root, remote_url=url) |
✅ Correct |
clone.py |
get_signing_identity(root, remote_url=url) |
✅ Correct |
pull.py |
get_signing_identity(root, remote_url=url) |
✅ Correct |
push.py |
get_signing_identity(root, remote_url=url) |
✅ Correct |
release.py |
get_signing_identity(root, url) (positional, not keyword) |
⚠️ Works but inconsistent style — should use remote_url= keyword |
domains.py |
get_signing_identity(repo_root) — resolved_hub computed above but not forwarded |
🔴 Bug — same class as the ls-remote 401 |
domains.py bug detail
Around line 796–800:
resolved_hub = hub_url or get_hub_url(repo_root) or "https://musehub.ai"
resolved_hub = resolved_hub.rstrip("/")
_validate_publish_url(resolved_hub)
token = get_signing_identity(repo_root) # BUG: resolved_hub not forwarded
The resolved_hub URL is known at this point. Not passing it means the identity lookup falls back to whatever the repo's [hub] url config says, which may be a different host.
musehub: proposalType legacy values
Migration 0070 renamed mergeStrategy values (state_overlay → overlay, etc.) but did not touch proposalType. The hub still creates and returns proposals with proposalType: "state_merge". The full set of legacy values in use needs to be enumerated and renamed to canonical form.
Scope: Alembic migration, ProposalType enum, musehub_proposals service, MCP tools, proposal templates, and any test fixtures using the legacy strings.
Phases
Phase 1 — Fix domains.py (muse repo)
Change line ~800 from:
token = get_signing_identity(repo_root)
to:
token = get_signing_identity(repo_root, remote_url=resolved_hub)
Add a regression test matching the TestSigningIdentityForwarding pattern established in tests/test_cmd_ls_remote.py.
Phase 2 — Normalize release.py call style (muse repo)
Change the three positional-arg call sites from:
token = get_signing_identity(root, url)
to:
token = get_signing_identity(root, remote_url=url)
No behavior change — positional order is correct — but explicit keyword makes the intent auditable and prevents future positional-arg drift.
Phase 3 — proposalType canonical rename (musehub repo)
- Enumerate all
proposalTypevalues currently in the DB and in the codebase. - Write migration 0071:
UPDATE musehub_proposals SET proposal_type = CASE ... END WHERE proposal_type IN (...legacy values...). - Update
ProposalTypeenum — removestate_*anddomain_*variants, keep only canonical names. - Update service layer (
musehub_proposals.py), MCP tools, Jinja templates, and test fixtures. - Verify no legacy strings remain in any Python source or template.
Phase 4 — Cross-host integration test (muse repo)
Add a test (or extend tests/test_wire_localhost.py) that:
- Configures a repo with a named remote pointing to a URL whose hostname differs from the repo's
[hub] url - Calls
ls-remote,fetch, anddomains publish(once Phase 1 lands) against that remote - Asserts the signing identity presented to the transport matches the remote hostname, not the default hub hostname
- Asserts no HTTP 401 is produced
This prevents regression across all callers as new commands are added.