KISS Sorcar — fourth task in the Muse-auth series. Development model: claude-fable-5. Independent read-only review and debugging: gpt-5.6-sol (three rounds via run_parallel, final verdict APPROVED).
Earlier tasks in this series built a Meta-Muse-style credential isolation layer (daemon-owned vault, opaque muse-sgt.* surrogates in the agent process, Sentinel egress policy at the network boundary, enabled by KISS_MUSE_AUTH=1; since September 2026 it is the default, with KISS_MUSE_AUTH=0 as the opt-out) and wired it into the Google/Notion/GitHub, Slack/Firecrawl/Brave, and Discord/Home Assistant/ntfy/Govee connectors. This task covers the seven remaining credentialed messaging connectors — Mattermost, Twitch, Zalo, LINE, Nextcloud Talk, BlueBubbles, and Synology Chat — and adds the one credential placement the vault could not represent before: secrets carried in a URL query parameter.
| Connector | Credential scheme | Vault representation | Host binding / notes |
|---|---|---|---|
| Mattermost | Personal access token, Authorization: Bearer |
{"kind":"bearer",...} |
Self-hosted: strictly origin-bound to scheme://url:port composed from config (type-strict: JSON booleans, floats, and non-http(s) schemes are malformed, never coerced). The uninstalled mattermostdriver SDK is replaced in Muse mode by a small REST shim, _MuseMattermostDriver, over MuseBoundarySession. POST /users/me/typing classifies as a read so the typing indicator cannot burn a one-shot write grant. |
| Twitch | OAuth Bearer + non-secret Client-ID header |
{"kind":"bearer",...} |
Fixed host api.twitch.tv. Both access_token and the unused client_secret are scrubbed from config; client_id metadata survives. |
| Zalo | Token in Zalo's custom access_token request header |
{"kind":"header","header":"access_token",...} |
Fixed host openapi.zalo.me. The vault's header-name rule now allows underscores. The agent sends a surrogate Bearer; the daemon emits the real access_token header and drops Authorization entirely. |
| LINE | Channel access token, Bearer | {"kind":"bearer",...} |
Fixed host api.line.me. Duck-typed _SdkLineApi/_MuseLineApi adapters: Muse mode builds plain JSON payloads with no linebot SDK import; legacy mode keeps the SDK. |
| Nextcloud Talk | HTTP Basic (username/password) | {"kind":"header","header":"Authorization","token":"Basic <b64>"} |
Origin-bound; consent-scoped insecure-host support for plain-HTTP servers. Credential validity requires both a successful HTTP status and OCS meta.statuscode — error responses also arrive inside an ocs envelope. |
| BlueBubbles | password= URL query parameter (macOS iMessage bridge) |
{"kind":"query","param":"password",...} (new kind) |
Origin-bound to the required server_url. POST /api/v1/message/query (a retrieval whose filters ride in the body) classifies as a read. Server responses count as success only with HTTP 200 and envelope status == 200. |
| Synology Chat | token= query parameter embedded in the incoming-webhook URL |
{"kind":"query","param":"token",...} |
The token is extracted from the webhook URL into the vault; the stored URL keeps only non-secret parts plus a muse migration marker. A tokenless URL without the marker is explicit user intent and never revives a stale vault token. |
Out of scope, with rationale recorded in the progress log: MS Teams (client-credentials token exchange needs daemon-side token acquisition), Telegram (token lives in the URL path), Matrix (async nio SDK), Twilio/Feishu/QQ/WeCom/DingTalk (SDKs absent or HMAC/app-secret exchange flows), Tlon (cookie login), IRC/email (non-HTTP), Signal/WhatsApp/iMessage-bridge-free/SimpleX/Nostr (local bridges without HTTP credential egress).
Query credentials are the most leak-prone placement: the secret is part of the URL, so it wants to appear in logs, in Location headers, in exception text, and in any consumer of the request line. The daemon treats the URL as credential-free everywhere except the final send:
Concretely, in muse_auth/daemon.py:
password= can never ride next to — or instead of — the real one.ExceptionClass contacting <credential-free URL>.Muse migration rewrites the stored webhook URL without its token= parameter, which makes a scrubbed config indistinguishable from one the user deliberately configured without a secret — and an ambiguous "tokenless" URL must not silently inherit whatever token is still in the vault. The scrub therefore writes a non-secret "muse": "1" marker:
| Config state | Vault has a token | Behavior |
|---|---|---|
URL carries token=… | any | Newest user intent: enroll it (replacing the vault entry), scrub the URL, add the marker, wire the boundary. |
| Tokenless URL, marker present | yes | Muse-scrubbed config: mint a surrogate and re-wire. |
| Tokenless URL, marker present | no | Vault was cleared: degrade to the direct path. |
| Tokenless URL, no marker | yes (stale) | Explicit tokenless intent: direct path, stale token never reaches the wire; authenticate_synology() with a tokenless URL also clears the vault entry. |
Re-wiring to the direct path goes through a _wire_direct() helper that also resets earlier Muse state (_muse/_surrogate/boundary session) — a rotation bug the new regression tests caught. _muse_authenticate is transactional: the embedded token is pre-validated with the vault's own rule before anything is written, and any later failure restores the pre-call config bytes (mode 0600).
gpt-5.6-sol reviewed read-only via run_parallel, instructed to report only demonstrable defects with reproductions (total review spend stayed far under the 50% budget cap). Every finding came with a working real-daemon/real-server reproduction.
slash/plus+question?percent% survived redaction as slash%2Fplus%2Bquestion%3Fpercent%25 inside requests' exception URL. Fixed with the generic class-plus-clean-URL error.{"status":401,"message":"bad password"} — validated a bad password into the vault. Fixed: HTTP 200 and envelope status == 200.ocs key, so envelope presence validated bad Basic credentials. Fixed: strict _validate_credentials() (HTTP status + OCS meta statuscode) in both connect() and the authenticate tool.participants/active misclassified as a read. The join creates an active participant session server-side; under a write-deny policy it still executed. Fixed: it is a write and needs a write grant, poll loop included.true into hostname "True" and scrubbed the token anyway. Fixed: type-strict URL composition.443.9 imported as 443) and failed mypy. Fixed: ports must be non-boolean integers or integer strings.The reviewer re-ran all prior reproductions plus fresh ones: A → A re-injects, A → B → B stays credential-free on every B request, A → unallowlisted B → A re-injects only on A, bodyless cross-host download hops still work, header-kind allowlisted redirects still carry the real header, and the port matrix (443, "443", 8065 accepted; 443.0, 443.9, true, junk rejected without scrubbing the plaintext token). No residual or new defects.
test_muse_auth_messaging.py: 62 tests pass — a real daemon subprocess plus a recording HTTP emulator for all seven messaging APIs (with OCS-401, non-JSON-500, on/off-site redirect, and two-hop chain endpoints); no mocks, patches, or fakes.uv run check --full passes: ruff, mypy, pyright, compileall, docs.KISS_MUSE_AUTH=0, legacy behavior is unchanged (verified by the reviewer's legacy-mode probe and the untouched legacy suites). Update: Muse-auth is now the default; unset means enabled on daemon-capable platforms.| Path | Change |
|---|---|
muse_auth/_common.py | Query-kind helpers (strip_url_query_param, valid_credential_param, origin helpers), SERVICE_HOSTS for the 7 services, protocol v4, read-classification rules (Mattermost typing, BlueBubbles message query), underscore header names. |
muse_auth/vault.py | resolve_credential() returns (placement, name, value); query kind stored/rotated like the others. |
muse_auth/daemon.py | Query-kind store validation, caller-param stripping, post-Sentinel injection, pinned-origin redirect chain, encoded-value-proof error text. |
muse_auth/client.py | MuseBoundarySession.request multipart files= support (Zalo upload). |
muse_auth/__main__.py | CLI import for all 7 services, incl. Basic folding (Nextcloud) and webhook-token extraction (Synology); required-URL and type validation. |
| 7 connector modules | Vault-first _wire_muse(), transactional authenticate tools, config scrubbing keeping metadata, Muse-aware clear/check tools, REST shims/adapters where SDKs are absent. |
test_muse_auth_messaging.py | New 62-test e2e suite (~1,900 lines). |
Progress log: tmp/PROGRESS.md. Review reports: tmp/review_round1_messaging.md, tmp/review_round2_messaging.md, tmp/review_round3_messaging.md (reviewer reproductions alongside).