Say so when a container has no credential for a collection - #94
Merged
Conversation
The credentials file landed in 0.15.0, but only for people who migrated onto it. A workload created before it goes on reading its frozen FIBER_SECRETS snapshot, and a collection authenticated after that workload started is not stale in the snapshot — it is absent. There is no 401, so nothing refreshes and nothing retries; the send fails before it is made, with "not signed in — open Section settings and sign in". In a container there are no Section settings, no window to sign in through, and usually a user who is signed in and whose token simply never reached the process. The 401 path has had source-aware advice since the file landed; the failure that happens first had none. It does now. `send_failure` names the collection and the reference, says which source the server reads, and — for a snapshot — that the source was frozen when the workload started and cannot pick a later sign-in up. It asks `secrets::has` rather than matching on the error text, so a credential that exists and is being rejected still gets the message it had. The desktop app has neither variable set and is unchanged. Two more places notice. The server reports its credential source at startup and warns there about every shared collection whose credential it cannot see — a warning, not a refusal to start, because under a file the value legitimately arrives later. And `list_sections` marks those collections `"credential": "missing"`, so an agent finds out before spending a call rather than after. Silently re-captured browser credentials reach the file too. On a 401 the app lifts a fresh one out of a hidden webview and deliberately does not write it to the keychain, because writing one costs a password prompt on a build that cannot hold an ACL. None of that reasoning carries over to the sealed file — the key is already cached for the life of the process and the write is a file write — but it was being skipped all the same, so a container only ever saw a browser credential change on an explicit sign-in. That is the same failure the file was added to end, left open for the one auth kind that cannot refresh itself. Rerunning scripts/toolhive.sh migrates a pre-0.15 workload, which nothing prompted anyone to do. It now says when it has, and that the fiber-secrets snapshot left behind is unread and holds whatever the credentials were the day it was taken.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
The Fiber MCP server under ToolHive returned
authentication failed: not signed in — open Section settings and sign infor a collection that worked fine in the app.Nothing had failed. The container reads
FIBER_SECRETS, a JSON snapshot read once into aOnceLockat startup. The workload was created 2026-08-24, when only one collection had a credential; the other was authenticated on 2026-09-03. A process's environment cannot change under it, so there was no mechanism by which that credential could ever have reached the running container.#92 already fixed the mechanism. What was missing was everything around it: no migration for workloads created before it, no detection that a shared collection has no credential, and an error message describing a desktop app while running in a container. That combination turned a one-line answer into a source dive.
Changes
The pre-send failure explains itself.
AuthError's text is written for the desktop app, and the 401 path has had source-aware advice since the credentials file landed — but the failure that happens first, before a request is ever sent, had none. Newsecrets::InjectedSource(None/Snapshot/File/Both) andmcp::send_failure, wired into both MCP send paths:Gated on
secrets::has(reference)rather than a match on the error text, so a credential that exists and is being rejected keeps the message it had. The desktop app has neither variable set and is unchanged.Startup notices.
report_credentialslogs the credential source and warns about every shared collection whose credential the process cannot see. A warning, not a refusal to start: under a credentials file the value legitimately arrives later, and refusing to serve the authenticated collections until every one of them is would be worse than the problem.list_sectionsmarks them with"credential": "missing", omitted when fine, plus the source advice appended once towarnings. An agent finds out before spending a call rather than after.Silent re-captures reach the file. On a 401 the app lifts a fresh browser credential out of a hidden webview and deliberately does not write it to the keychain, because that costs a password prompt on a build that cannot hold an ACL. None of that carries over to the sealed file — the key is already cached for the life of the process and the write is a file write — but it was being skipped all the same, so a container only ever saw a browser credential change on an explicit sign-in. That is the same failure the file was added to end, left open for the one auth kind that cannot refresh itself.
Migration. Rerunning
scripts/toolhive.shmigrates a pre-0.15 workload, and nothing prompted anyone to do it. It now says when it has, and that thefiber-secretssnapshot left behind is unread and holds whatever the credentials were the day it was taken. New "Migrating fromFIBER_SECRETS" section indeploy/toolhive.md, linked from the snapshot section and the README.Tests
Three in
secrets.rs, covering the source mapping and the advice that hangs off it. The mapping is split into a puresource_ofso they set no process-wide environment variables — the same way the existing tests there avoid it.Verification
Run in a
rust:1.90container matching CI:cargo fmt --check— passcargo clippy --no-default-features --all-targets -- -D warnings— exit 0cargo test --no-default-features— 120 passed, 0 failedcargo check --all-targetswith GUI features — exit 0, no warningsThat last one is not in CI. The clippy step runs
--no-default-features, so it never compilesbrowser.rsor thelib.rsGUI module — where four of the edits in this PR are. Onlycargo testreaches them. Worth closing separately.