gabriel / muse public
Open #75 Bug
filed by gabriel human · 17 days ago

muse mist create --sign silently fails to sign outside a hub-configured directory

0 Anchors
Blast radius
Churn 30d
0 Proposals

muse mist create --sign silently fails to sign outside a hub-configured directory

Status

All three phases complete (2026-07-06). Implemented TDD-first on branch task/mist-sign-hub-context:

  • Phase 1 (red): MISTSIGN_01/02/03 added to tests/test_mist_cli.py, confirmed failing against pre-fix code — MISTSIGN_01 got signed: False, MISTSIGN_02 got get_signing_identity called with no remote_url, MISTSIGN_03 got silence on stderr instead of a warning.
  • Phase 2 (fix): run_create's signing block now resolves hub_url the same way --push does (hub_override, else repo-local config via _get_hub_url()) and passes it as get_signing_identity(remote_url=hub_url). Prints a clear stderr warning when no hub context resolves at all, instead of silent failure. One correction found mid-implementation: a fresh auth keygen alone saves handle: "" — only auth register sets a real handle, which resolve_signing_identity requires to be non-empty. MISTSIGN_01 had to additionally mock register's network calls (mirroring test_cmd_auth_phase5.py's _run_keygen_and_register pattern) to produce a fully valid identity, not keygen alone.
  • Phase 3 (regression + audit): all 95 tests in test_mist_cli.py pass (including pre-existing M1/M2/M3). grep -rn "get_signing_identity()" muse/ now returns zero real call sites (only a comment referencing the fixed bug). muse code test --json run for downstream impact.

Committed as sha256:f7505032a4d5531c801c071fbd3f51880c7cb3d007077e724bbfd4c44277f586 on branch task/mist-sign-hub-context, not yet merged to dev.

Background

Discovered live while creating a mist to share on social channels: muse mist create ... --sign --push --hub https://staging.musehub.ai returned "signed": false with no error, no warning — despite muse auth whoami --hub https://staging.musehub.ai confirming a fully configured identity (handle: gabriel, key_set: true).

Root cause, traced and confirmed live (not guessed):

muse/cli/commands/mist.py:377 calls get_signing_identity() with zero arguments inside run_create's signing block:

if do_sign:
    try:
        from muse.cli.config import get_signing_identity
        from muse.core.keypair import sign_bytes as _sign_bytes
        _signing = get_signing_identity()          # <-- no remote_url
        if _signing:
            gpg_signature = _sign_bytes(_signing.private_key, content)
            signed = True
    except Exception as exc:
        print(f"⚠️  Could not sign mist: {sanitize_display(str(exc))}", file=sys.stderr)

get_signing_identity(repo_root=None, remote_url=None, agent_id=None)'s docstring states its resolution order falls back to "Hub URL from [hub] url in .muse/config.toml" when remote_url is not supplied — i.e. without an explicit remote_url, it can only resolve an identity if the current working directory happens to be inside a repo with a locally configured hub. --hub (which the user explicitly passed) is never threaded into this call at all — it's read into hub_override and only used later, inside the separate if do_push: block, for _require_hub(hub_override).

Confirmed live from /tmp (a directory with no .muse/config.toml):

>>> get_signing_identity()
None
>>> get_signing_identity(remote_url='https://staging.musehub.ai')
SigningIdentity(handle='gabriel', private_key=<...Ed25519PrivateKey...>)

Same identity, same machine, same keychain mnemonic — the only difference is whether a hub URL is threaded through. The correct pattern already exists elsewhere in the same file, in _hub_api (line 176): get_signing_identity(remote_url=server_root).

Audited for the same anti-pattern across the whole muse/ tree — grep -rn "get_signing_identity()" muse/ returns exactly one hit: this call site. The bug is isolated, not systemic.

Why existing tests didn't catch this: test_M2_run_create_sign_no_import_error in tests/test_mist_cli.py mocks get_signing_identity unconditionally (mock.patch(..., return_value=fake_signing)), so it never observes what arguments the real call site passes — it would pass even if the call were get_signing_identity(), get_signing_identity(remote_url=None), or get_signing_identity(remote_url="garbage"). No existing test asserts the hub context is actually threaded through.

Goal

muse mist create --sign reliably produces a signed mist whenever any valid hub context is resolvable — via --hub, via a repo-local .muse/config.toml, or (when --push is also given) via --push's own hub resolution. In the one case signing genuinely cannot resolve an identity (no --hub, no repo-local config, and no --push to borrow a hub from), the CLI must print a clear, actionable warning instead of silently returning "signed": false.

"Done" means: --sign --push --hub <url> signs correctly regardless of the caller's current working directory, and --sign alone (no --push) still signs when --hub is given or a repo-local hub is configured.

Out of Scope

  • Signing behavior of other mist subcommands (fork, update, push) — this ticket is scoped to create only, where the bug was found and reproduced.
  • Any broader refactor of get_signing_identity's resolution order or the identity/keychain system itself — the fix is threading an already-correct pattern into one call site, not redesigning identity resolution.

Phases

Phase 1 — Red: reproduce the bug with a real (non-mocked) test

Existing coverage mocks get_signing_identity unconditionally, which is exactly why this shipped unnoticed. The new test must exercise the real resolution path end-to-end (real keychain-backed identity, real --hub flag, no mocking of get_signing_identity itself) so it fails against current code and passes only once the real argument-threading bug is fixed.

  • MISTSIGN_01run_create with --sign --hub <url> (no --push), invoked from a tmp_path directory with no .muse/config.toml present, and a mocked keychain returning a known test mnemonic: assert signed is True in the JSON output. This must fail against current code (get_signing_identity() returns None because cwd has no hub config) and pass after the fix.
  • MISTSIGN_02 — mock-based assertion that get_signing_identity is called with remote_url equal to the resolved hub URL, for both --sign --push --hub <url> and --sign --hub <url> (no push). Asserts the call arguments, not just that signing eventually succeeds — this is what M2 should have asserted originally and didn't.
  • MISTSIGN_03--sign with no --hub and no repo-local config and no --push: assert signed is False and a clear warning is printed to stderr (e.g. "Could not sign mist: no hub configured") — not silence.

Deliverable: ✅ all three tests written and confirmed red against pre-fix code — MISTSIGN_01 got signed: False, MISTSIGN_02 got a call with no remote_url, MISTSIGN_03 got silent stderr — before any Phase 2 code was written.

Phase 2 — Fix: thread hub context into the signing path

  • Reused _get_hub_url() (already non-raising — returns hub_url | None without _require_hub's hard SystemExit) rather than adding a new helper.
  • run_create's signing block now resolves hub_url via: hub_override (the --hub flag) first, else _get_hub_url()'s repo-local lookup, else None. Passed as get_signing_identity(remote_url=hub_url).
  • When do_push is also True, the later _require_hub(hub_override) call resolves identically (same hub_override-first, same underlying _get_hub_url() fallback) — no divergent double-resolution.
  • Added a clear stderr warning ("no hub configured — pass --hub or run inside a repo with a configured hub") on the None case, not just on exception.

Deliverable: ✅ MISTSIGN_01, MISTSIGN_02, MISTSIGN_03 all green.

Phase 3 — Regression sweep and audit

  • Full test_mist_cli.py95 passed, including pre-existing M1/M2/M3.
  • grep -rn "get_signing_identity()" muse/ — zero real call sites remain (one match is a comment in the fix referencing the historical bug, not a call).
  • muse code test --json run post-commit — reported "no changes detected" (comparison baseline already includes the committed fix).

Deliverable: ✅ MISTSIGN_04 — full regression pass, zero new failures, zero remaining unscoped get_signing_identity() calls.


Acceptance Criteria

  • muse mist create <file> --sign --push --hub <url> produces "signed": true regardless of the caller's current working directory.
  • muse mist create <file> --sign --hub <url> (no --push) also produces "signed": true under the same condition.
  • muse mist create <file> --sign with no hub context available anywhere prints a clear warning and returns "signed": false — never silent.
  • All of MISTSIGN_01 through MISTSIGN_04 pass; the full test_mist_cli.py suite remains green.
  • No new zero-argument get_signing_identity() call sites exist anywhere in muse/.

Implementation Order

Phase 1 → Phase 2 → Phase 3. TDD by the book: the red tests in Phase 1 must be written and confirmed failing before any fix code in Phase 2 is written.

Activity
gabriel opened this issue 17 days ago
No activity yet. Use the CLI to comment.