gabriel / muse public
Closed #20 Bug enhancement
filed by gabriel human · 139 days ago

Signing identity resolution and proposalType canonical cleanup

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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)

  1. Enumerate all proposalType values currently in the DB and in the codebase.
  2. Write migration 0071: UPDATE musehub_proposals SET proposal_type = CASE ... END WHERE proposal_type IN (...legacy values...).
  3. Update ProposalType enum — remove state_* and domain_* variants, keep only canonical names.
  4. Update service layer (musehub_proposals.py), MCP tools, Jinja templates, and test fixtures.
  5. 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, and domains 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.

◎ Activity2
●
gabriel opened this issue 139 days ago
○
gabriel 1 day ago

Implemented and shipped — commit c5d88af1 on dev/main (pushed to local/staging/production). Scope: muse-repo phases only (1, 2, 4). Phase 3 (musehub proposalType legacy-value rename) is entirely musehub-side and out of scope for this repo — see note at the bottom.

Phase 1 — domains.py bug

The ticket named one occurrence (around line 796–800 in an earlier revision). The audit against the current file found three — run_publish, run_update, and run_delete all independently compute resolved_hub (from --hub, the repo's [hub] url, or the musehub.ai default) but then called get_signing_identity(repo_root) without forwarding it. All three fixed to get_signing_identity(repo_root, remote_url=resolved_hub).

Phase 2 — release.py call style

Normalized the three get_signing_identity(root, url) positional calls to get_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] url and a named staging remote deliberately point at different hostnames, driving ls-remote, fetch, and domains 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 (unlike tests/test_wire_localhost.py, which only runs against a real localhost:1337 and 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 for release.py's call-style normalization, following the TestSigningIdentityForwarding pattern already established in tests/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.py together and re-running: all 4 test_domains_signing_identity.py tests failed with remote_url=None instead of the target host — restored, all green. Separately confirmed the cross-host guard turns red when only the domains.py fix is reverted (ls-remote/fetch stayed 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 proposalType legacy-value rename (migration 0070 renamed mergeStrategy values but missed proposalType; the hub still creates/returns proposalType: "state_merge") lives entirely in the musehub repo — Alembic migration, ProposalType enum, musehub_proposals service, 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.

○
gabriel 1 day ago

Filed the musehub-side follow-up: musehub#229 — proposalType canonical cleanup (state_merge → merge), with the full file:line audit and phase breakdown.

✓
closed this issue 1 day ago