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.
Filed the musehub-side follow-up: musehub#229 — proposalType canonical cleanup (state_merge → merge), with the full file:line audit and phase breakdown.
Implemented and shipped — commit
c5d88af1ondev/main(pushed to local/staging/production). Scope: muse-repo phases only (1, 2, 4). Phase 3 (musehubproposalTypelegacy-value rename) is entirely musehub-side and out of scope for this repo — see note at the bottom.Phase 1 —
domains.pybugThe ticket named one occurrence (around line 796–800 in an earlier revision). The audit against the current file found three —
run_publish,run_update, andrun_deleteall independently computeresolved_hub(from--hub, the repo's[hub] url, or themusehub.aidefault) but then calledget_signing_identity(repo_root)without forwarding it. All three fixed toget_signing_identity(repo_root, remote_url=resolved_hub).Phase 2 —
release.pycall styleNormalized the three
get_signing_identity(root, url)positional calls toget_signing_identity(root, remote_url=url). No behavior change (positional order was already correct, as the ticket noted) — makes the intent auditable and guards against a future signature change silently breaking identity resolution without a loud error.Phase 4 — cross-host regression guard
New
tests/test_cross_host_signing_identity.py: a repo whose default[hub] urland a namedstagingremote deliberately point at different hostnames, drivingls-remote,fetch, anddomains publish— the three callers the ticket names — against the named remote, asserting each resolves identity against the remote's own hostname, never the default hub's. Mocked transport/network throughout, so it runs in CI without a live hub (unliketests/test_wire_localhost.py, which only runs against a reallocalhost:1337and auto-skips otherwise) — any future remote-talking command that forgets to forward its resolved URL will fail this test immediately.Tests
tests/test_domains_signing_identity.py— one regression test per fixed call site (publish/update/delete) plus one forrelease.py's call-style normalization, following theTestSigningIdentityForwardingpattern already established intests/test_cmd_ls_remote.py.tests/test_cross_host_signing_identity.py— the Phase 4 cross-host guard described above.Confirmed genuine RED by reverting
domains.py+release.pytogether and re-running: all 4test_domains_signing_identity.pytests failed withremote_url=Noneinstead of the target host — restored, all green. Separately confirmed the cross-host guard turns red when only thedomains.pyfix is reverted (ls-remote/fetchstayed green, since the ticket's own audit already marked those correct) and green again once restored.Regression: 703 tests across the domains, release, and ls-remote test suites — all green, no regressions.
Out of scope — Phase 3 (musehub repo)
The
proposalTypelegacy-value rename (migration 0070 renamedmergeStrategyvalues but missedproposalType; the hub still creates/returnsproposalType: "state_merge") lives entirely in the musehub repo — Alembic migration,ProposalTypeenum,musehub_proposalsservice, MCP tools, and proposal templates. None of that exists in this repo. Recommend filing a musehub-tracked issue for it rather than folding it into this one, since it's a fully independent piece of work with its own migration/test surface.