cgcardona / muse public
feat main #13 / 100

feat: muse auth + hub + config — paradigm-level identity architecture with security hardening

* feat: introduce muse auth, muse hub, muse config — paradigm-level identity architecture

Replaces the flat [auth] token-in-repo-config pattern with a three-command identity paradigm designed around Muse's two primary users: humans and agents.

Core architectural shift ------------------------ - Credentials move out of .muse/config.toml and into ~/.muse/identity.toml (mode 0o600, never read by the snapshot engine, never accidentally committed). - The repository knows *where* the hub is ([hub] url in config.toml). The machine knows *who you are* (~/.muse/identity.toml). These two concerns are explicitly separated. - Agents and humans are first-class typed identities, not just bearer tokens.

New: muse/core/identity.py -------------------------- Global identity store. IdentityEntry carries type ("human" | "agent"), name, hub-assigned id, token, and capabilities (agent capability strings). All operations are synchronous. Token is never logged.

New: muse auth -------------- login [--token TOKEN] [--hub HUB] [--agent] — human or agent auth flow whoami [--json] — structured identity display logout [--hub HUB] — remove credentials

New: muse hub ------------- connect <url> — anchor repo to MuseHub fabric, write [hub] url status [--json] — show connection + identity (agent-safe JSON mode) disconnect — remove hub association from this repo ping — HTTP health-check to hub endpoint

New: muse config ---------------- show [--json] — display full config (credentials never included) get <key> — dotted key lookup (user.name, hub.url, domain.*) set <key> <val> — typed setter with namespace guards edit — open .muse/config.toml in $EDITOR

Updated: muse/cli/config.py ---------------------------- - Added UserConfig, HubConfig TypedDicts; removed AuthEntry. - get_auth_token() now resolves via identity store (hub URL → identity.toml). - Added get_hub_url / set_hub_url / clear_hub_url helpers. - Added get_config_value / set_config_value for dotted-key access. - _dump_toml order: [user], [hub], [remotes.*], [domain].

Updated: muse init config template ------------------------------------ - Removed [auth] token stub — replaced with [hub] commented example. - Added [user] type = "human" field. - Inline guidance pointing to `muse hub connect` and `muse auth login`.

All: mypy zero errors, typing_audit zero violations, 89 core tests green.

* security: harden identity store against TOCTOU, symlink, and cleartext attacks

Seven vulnerabilities found in the initial auth implementation and patched.

TOCTOU race (identity.py) write_text() created the file world-readable (default umask 0o644), then chmod() restricted it — any process watching ~/.muse/ could read the token in that window. Fix: os.open() + os.fchmod(fd, 0o600) before the first byte is written, followed by os.fdopen() to write, followed by os.replace() for atomic swap.

Non-atomic write (identity.py) A kill signal during write_text() left a partial TOML file — next read silently returns an empty identity store. Fix: write to a mkstemp() temp file in the same directory, then os.replace() atomically renames it over the target. Old file stays intact on crash.

Symlink attack (identity.py) An attacker pre-placing a symlink at ~/.muse/identity.toml causes write_text() to follow it and overwrite whatever file the user has write access to. Fix: path.is_symlink() guard before any write; raises OSError on detection.

Directory permissions (identity.py) mkdir(parents=True, exist_ok=True) without a mode argument created ~/.muse/ world-traversable (0o755). Local users could list the directory. Fix: os.chmod(dir_path, stat.S_IRWXU) after mkdir — restricts to 0o700.

http:// cleartext token transmission (hub.py) _normalise_url() only added https:// when no scheme was present. An explicit http:// URL was stored and used, causing bearer tokens to be sent in cleartext on every push/pull. Fix: ValueError raised for any http:// URL; user shown the https:// equivalent.

TOML injection in capabilities (identity.py) Capability strings were written raw into TOML array values — a capability containing " or \ would produce malformed TOML and corrupt the identity file. Fix: _toml_escape() applied to every capability string in _dump_identity().

Misleading warning on hub switch (hub.py) "To keep the old credentials, run: muse auth logout" was inverted — logout removes credentials, not keeps them. Fix: message now reads "Your credentials remain … To remove them: muse auth logout".

Also: OSError from _save_all is now caught in auth login and surfaced as a clean ❌ message instead of a Typer traceback dump.

All fixes verified: mypy zero errors, typing_audit zero violations, 89 tests green.

* security: second-pass hardening — redirects, cleartext, locking, hostname normalisation

Nine additional vulnerabilities patched. All verified with live tests.

Redirect-with-credentials (transport.py) urllib.request.urlopen follows HTTP redirects by default, including HTTPS→HTTP downgrades and cross-host redirects, both of which would forward the Authorization: Bearer header to an unintended recipient. Fix: _NoRedirectHandler raises HTTPError on any redirect; _STRICT_OPENER is used for every request in HttpTransport._execute.

Token over cleartext HTTP in transport (transport.py) A remote URL stored as http:// would cause the bearer token to be sent in plaintext on every push/pull — existing muse remote add had no HTTPS guard. Fix: _build_request raises TransportError before adding the Authorization header when the URL scheme is not https://.

HTTPS enforcement in set_hub_url + config set (config.py) muse config set hub.url http://... bypassed the _normalise_url check in hub connect. Fix: set_hub_url now raises ValueError on non-HTTPS URLs; set_config_value routes hub.url writes through set_hub_url.

Redirect refusal in ping (hub.py) _ping_hub used urllib.request.urlopen (default redirect-following). Fix: dedicated _NoRedirectHandler + _PING_OPENER; any redirect surfaces as an error with the attempted redirect target shown.

Userinfo in hub URL key (identity.py) https://user:pass@musehub.ai stored "user:pass@musehub.ai" as the hostname identity key, embedding credentials in config.toml. Fix: _hostname_from_url strips userinfo (user:pass@) before the hostname is extracted.

Case-insensitive hostname normalisation (identity.py) DNS is case-insensitive; musehub.ai and MUSEHUB.AI are the same host but would create two separate identity entries, causing lookup misses. Fix: _hostname_from_url normalises to lowercase unconditionally.

Concurrent write race (identity.py) Two simultaneous muse auth login calls (e.g. parallel agents) both read the identity file, both modify in-memory, last writer wins and silently erases the other's entry. Fix: _identity_write_lock (fcntl.LOCK_EX on ~/.muse/.identity.lock) wraps the read-modify-write cycle in save_identity and clear_identity.

TOML parse-error log leaks token fragment (identity.py) tomllib error messages include the offending line — a corrupted identity file could expose a token fragment in the log output. Fix: _load_all logs only type(exc).__name__, never exc itself.

Shell history warning for --token CLI flag (auth.py) Tokens passed via --token appear in ~/.zsh_history and ps aux. Fix: login detects whether the token came from the CLI flag (token is not None and MUSE_TOKEN env var is absent) and emits a ⚠️ warning to stderr recommending the env-var pattern instead.

All: mypy zero errors, typing_audit zero violations, 89 tests green.

---------

Co-authored-by: Gabriel Cardona <gabriel@tellurstori.com>

G Gabriel Cardona <cgcardona@gmail.com> · 9h ago Mar 20, 2026 · 80353726 · parent 9fb2d692
oldest
newest 88%

Comments

0

No comments yet. Be the first to start the discussion.