KISS Sorcar — third task in the Muse-auth series. Development model: claude-fable-5. Independent read-only review and debugging: gpt-5.6-sol (eight rounds, all via run_parallel, final verdict APPROVED).
The first two tasks in this series built a Meta-Muse-style credential isolation layer for KISS connectors: a daemon-owned vault holds real tokens, agent processes hold only opaque surrogates (muse-sgt.<service>.<hex>), and a Sentinel policy engine authorizes every request at the network boundary where the surrogate is swapped for the real credential. This task extends that boundary to the four remaining credentialed connectors whose APIs do not use plain Authorization: Bearer semantics, and hardens the URL/host validation stack that self-hosted enrollment depends on.
| Connector | Credential scheme | Vault representation | New boundary capability |
|---|---|---|---|
| Discord | Authorization: Bot <token> |
{"kind":"header","header":"Authorization","token":"Bot <tok>"} |
Header-kind credentials (built for Brave Search in task 2) carry the scheme prefix inside the stored value, so the daemon emits the exact non-Bearer header. POSTs to /typing classify as reads: the ephemeral typing indicator must not burn a one-shot write grant meant for the message send that follows. |
| Home Assistant | Bearer, but always self-hosted, often plain http:// on a LAN |
{"kind":"bearer",...} + enrolled origin host:port |
Consent-scoped insecure hosts. The Sentinel normally refuses plain-HTTP egress for a credential. A host enrolled from an http:// base URL at consent time is recorded in the vault as an insecure host; only that exact origin may be reached over HTTP. Daemon protocol bumped to v3 so a pre-upgrade daemon (which would silently drop the field and deny every request) is detected and replaced. |
| ntfy | Optional Bearer; public ntfy.sh works tokenless | {"kind":"bearer",...} + enrolled origin |
Tokenless configs stay on the legacy direct path (nothing to protect). A token is strictly origin-bound — a private-server token can never be spent against public ntfy.sh, and vice versa. Explicit token: "" in config is authoritative: it clears a stale vault entry rather than reviving it. |
| Govee | Govee-API-Key: <key> header, key from $GOVEE_API_KEY |
{"kind":"header","header":"Govee-API-Key",...} |
The CLI (govee.py) enrolls the environment key once, then removes it from its own process environment; POST /device/state (a query that merely names the device) classifies as a read while /device/control (actuation) stays a write. Ambiguous failures (daemon reply lost) are never replayed for writes. |
Authorization: Bot <token>; Home Assistant and ntfy use bearer-kind credentials plus a per-credential enrolled origin.Self-hosted services (Home Assistant, ntfy, Firecrawl) have no built-in host: SERVICE_HOSTS maps them to () and the credential is bound at consent time to exactly one host:port origin. That made the URL/host validation stack security-relevant, and most review findings across the eight rounds concentrated there. The final stack, applied identically at the CLI, the connector authenticate tools, the daemon enrollment path, and Sentinel request matching:
valid_http_url(url) # http(s) scheme, no userinfo, well-formed port,
# hostname validated per DNS label
canonical_host(host) # lowercase + removesuffix(".") — exactly ONE root dot
canonical_host_entry(e) # host[:port] normalization (leading-zero ports, brackets)
url_origin_entry(url) # "host:port" with scheme-default port applied
_invalid_hosts_reason(hs) # daemon-side gate before anything is stored
Host classes verified to agree across every layer: single-label names (localhost), multi-label FQDNs, one trailing DNS root dot (example.com.), IPv4, bracketed IPv6, IPv4-mapped IPv6 ([::ffff:127.0.0.1]), and scoped IPv6 with RFC-6874 ZoneIDs ([fe80::1%25eth-0]). Malformed classes rejected at every layer: empty DNS labels (bad..example, localhost..), 64-char labels, 254-char hostnames, label-edge hyphens, userinfo URLs, malformed ports, and bracketed non-IP bodies.
gpt-5.6-sol ran read-only over the staged tree with instructions to report only demonstrable problems (command + observed output). Every accepted finding came with a working reproduction; none was speculative. Highlights per round:
| Round | Representative real findings (all fixed with regression tests) |
|---|---|
| 1 | Non-transactional HA/ntfy enrollment left plaintext behind; vault replacement not generation-bound (redirect race could spend an old token under a new host scope); ntfy credential valid for public ntfy.sh; Govee kept its real key in the process environment; header values reflected into error text. |
| 2 | In-flight old-generation requests could spend rotated credentials (fixed with a per-request generation nonce pinned across redirect hops); host-vs-origin binding gaps; Google refresh obeyed HTTPS_PROXY. |
| 3 | Malformed ports raised raw ValueError mid-migration (Firecrawl destructive partial migration); Govee replayed a control POST after a lost daemon reply; ntfy exposed tools despite failed wiring. |
| 4 | Firecrawl's builtin cloud host let a rejected self-host key reach the cloud API (now strictly origin-bound); valid_http_url accepted userinfo; IPv4-mapped IPv6 layer disagreement. |
| 5–6 | CLI import bypassed URL validation; scoped-IPv6 ZoneID and DNS-label edge cases; JSON null stringified to "None" and rejected. |
| 7 | (1) rstrip(".") collapsed localhost.. into a valid name that then failed deep inside urllib3; (2) a null ntfy server imported without enrolling the ntfy.sh:443 default origin, leaving the migrated token unusable; (3) the null-URL exception was a broad truthiness test, accepting false/0/[] and Home Assistant's required null base_url. |
| 8 | None. Verdict: APPROVED. |
canonical_host() switched from rstrip(".") to removesuffix("."): example.com.. now canonicalizes to the still-malformed example.com. and fails per-label validation everywhere, while the legitimate FQDN spelling example.com. keeps working end to end (verified with a live boundary request to a localhost.:port server)._import_hosts() substitutes https://ntfy.sh when no server is configured, enrolling ntfy.sh:443 — the same default the connector loader applies — so a migrated public-cloud token is actually usable.base_url is rejected when missing; non-string falsy JSON values are malformed configs, not "use the default".connect()/_wire_muse() paths of all three self-hosted connectors now validate the base URL before any credential state change: a malformed legacy URL can no longer enroll the token into an unmatchable host scope or scrub the plaintext copy.src/kiss/tests/agents/third_party_agents/test_muse_auth_devices.py: 46 tests, SEA-style — real daemon subprocess, recording HTTP emulators for the Discord/HA/ntfy/Govee APIs, a LAN-addressed server for insecure-host consent, rogue redirect targets, and a connection-refusing port. No mocks.uv run check --full passes: dependency sync, API generation, compileall, Ruff, mypy, pyright.KISS_MUSE_AUTH=0 remains byte-identical. (Update: Muse-auth is now the default; unset means enabled on daemon-capable platforms.)13 staged paths, +3,535 / −158 lines: muse_auth/ core (_common.py, vault.py, sentinel.py, daemon.py, client.py, __main__.py), four connectors (discord_agent.py, homeassistant_agent.py, ntfy_agent.py, govee.py), shared _channel_agent_utils.py, prior-task connector touch-ups (firecrawl_agent.py), and the new 1,889-line device test suite.