Skip to content

Say so when a container has no credential for a collection - #94

Merged
AndersRobstad merged 1 commit into
mainfrom
post-membership-user-projects
Sep 4, 2026
Merged

Say so when a container has no credential for a collection#94
AndersRobstad merged 1 commit into
mainfrom
post-membership-user-projects

Conversation

@AndersRobstad

Copy link
Copy Markdown
Collaborator

What happened

The Fiber MCP server under ToolHive returned authentication failed: not signed in — open Section settings and sign in for a collection that worked fine in the app.

Nothing had failed. The container reads FIBER_SECRETS, a JSON snapshot read once into a OnceLock at 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. New secrets::InjectedSource (None/Snapshot/File/Both) and mcp::send_failure, wired into both MCP send paths:

No credential for collection "Kvist Prod" (6605308b-…:auth). This server reads credentials from FIBER_SECRETS, a snapshot taken when the workload started, so a collection signed into afterwards is not in it. Re-export and replace the workload, or switch to the credentials file the app keeps current — see deploy/toolhive.md.

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_credentials logs 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_sections marks them with "credential": "missing", omitted when fine, plus the source advice appended once to warnings. 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.sh migrates a pre-0.15 workload, and nothing prompted anyone to do it. 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. New "Migrating from FIBER_SECRETS" section in deploy/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 pure source_of so they set no process-wide environment variables — the same way the existing tests there avoid it.

Verification

Run in a rust:1.90 container matching CI:

  • cargo fmt --check — pass
  • cargo clippy --no-default-features --all-targets -- -D warnings — exit 0
  • cargo test --no-default-features — 120 passed, 0 failed
  • cargo check --all-targets with GUI features — exit 0, no warnings

That last one is not in CI. The clippy step runs --no-default-features, so it never compiles browser.rs or the lib.rs GUI module — where four of the edits in this PR are. Only cargo test reaches them. Worth closing separately.

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.
@AndersRobstad
AndersRobstad merged commit 166f130 into main Sep 4, 2026
4 checks passed
@AndersRobstad
AndersRobstad deleted the post-membership-user-projects branch September 4, 2026 12:19
@github-actions github-actions Bot mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant