Say what an upstream credential refusal actually was #347
No reviewers
Labels
No labels
burndown-2026-06
burndown-2026-08
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra!347
Loading…
Reference in a new issue
No description provided.
Delete branch "mcp-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #340, which opens with two questions before any code. Answering both, recommending against the browser flow, and shipping the part that is useful either way.
The questions
1. Does the browser flow belong in umbra? Recommend no.
umbra is a policy-free engine that holds no state. It has never opened a browser, never persisted a credential, and its whole value proposition is that a reviewer can read one guardfile and know the boundary. An interactive OAuth flow adds a listener, a redirect, and a token store, and every one of those is a thing a reviewer now has to trust separately.
The alternative already works: something else mints the token, umbra reads it through a value chain. That is what
auth bearer { value ssm "..." }does today, and it keeps the acquisition problem outside a component whose job is refusal.2. Where would a refresh token live? It does not arise under the above, which is the point of answering the first one first. A refresh token rotating on every use fits SSM poorly, and #340 already noticed that. The best fix for a bad answer is a question you do not have to ask.
What this PR ships
The gap was silent. A 401 surfaced as the SDK's bare word
Unauthorized, which does not say whether a credential was sent at all, and does not say the server asked for OAuth. The operator inferred both.The header transport already sees the response, so it now records a 401/403 and its
WWW-Authenticatechallenge:Three distinctions it now makes:
authdeclared, versus a declared value that resolved and was rejected - different problems with different fixesThe transport wrapper is now always installed rather than only when a header is declared, because the no-credential case is exactly the one worth reporting and it previously had no observer.
Scope
This does not close #340. It removes the silence, records the recommendation, and leaves the decision with you. If you want the browser flow in umbra after all, the disagreement is worth having explicitly rather than by my building it.
go test ./...,golangci-lint run ./...,pre-commit run --all-filespass.