Repository navigation
Description
Relocated from dezren39/darwin-nix#300 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).
Parent: #253
Summary
#253 implements the authorization interface: the OAuth endpoints exist, /mcp
acts as a resource server, credentials are accepted, and the shape matches the
spec. Deliberately, it authorizes everyone — present anything and the answer
is yes.
That is the right first move, because it separates two jobs that are usually
tangled and have completely different risk profiles. Making the interface exist
is mechanical, unblocks every conformance row that only checks the interface is
there and well-formed, and cannot break anybody — a daemon that authorizes
everyone is exactly as secure as today's daemon, which authorizes everyone by
having no interface at all.
This issue is the second job: the conformance rows that require an invalid
credential to be rejected. Those rows cannot pass against a permissive
interface by construction, and they are the ones that change runtime behaviour
and can lock a user out of their own daemon.
Nothing is blocked on this issue. It is not a precondition for any surface,
transport or feature. A permissive interface is a complete answer for a local
daemon, and remains one until somebody actually wants rejection.
Original notes
if an interface is not to spec because you must accept a user and password,
accept a user and password and tell them good job, you're authorized. if the
spec requires the interface and that invalid fails then go ahead and have a
separate issue to make the correct interface have the correct behavior.
Current state
No interface and no validation. mcpx has no OAuth client, /mcp performs no
authorization, and the conformance matrix (docs/spec/conformance-matrix.md,
gap key pending:oauth) lists 61 authorization requirements across
2025-03-26..2026-07-28 that are unimplemented (#253).
Configured auth is parsed and dropped. mcpauth.Resolve has no caller
(#240) and the per-server auth: block is parsed and never applied (#205). So
there is already credential configuration with no credential use — which
means the permissive interface is closer than it looks, and real validation has
existing structure to hook into.
The local model is unauthenticated on purpose. The daemon is explicitly
unauthenticated with socket file permissions as the access control
(internal/daemon/server.go:214, internal/daemon/origin.go:13,
internal/api/openapi.go:40). That remains correct and this issue does not
change it.
The split, concretely
Of #253's 61 rows, they divide by what a test actually asserts:
| row asserts | satisfied by | owner |
|---|---|---|
| the endpoint exists and is well-formed | a permissive interface | #253 |
| metadata discovery returns the right document shape | a permissive interface | #253 |
| a valid credential is accepted | a permissive interface (accepts all) | #253 |
| an invalid credential is refused | real validation | this issue |
| an expired token is refused | real validation | this issue |
| a token for the wrong resource is refused | real validation | this issue |
| scope is enforced | real validation | this issue |
Splitting the matrix along that line is the first task here, because it also
tells #253 exactly how far it can get — and the answer is probably "most of
them", which is the point.
Approaches
Approach A — least recommended: skip the permissive stage, build validation first
Do it "properly" in one go.
This is the default instinct and it is why auth work stalls. It couples a large
behavioural change to a mechanical interface change, so no conformance row moves
until all of it lands, and the first thing that ships is also the first thing
that can lock somebody out. Rejected — it is the shape #253's split exists to
avoid.
Approach B — middle: validation behind a setting, default off
One auth.enforce flag. Off, everything is authorized; on, credentials are
checked.
Simple and most of the value. Its weakness is that it is a single switch over a
set of independent checks — signature, expiry, audience, scope — which in
practice land at different times and have different confidence levels. One flag
means either shipping them as a block (Approach A again, with a flag) or having
the flag mean different things in different releases.
Approach C — recommended: per-check enforcement, defaulting to permissive, with the permissive answer always logged
Each validation step is independently enforceable and independently defaulted
off. Signature verification can be real while scope enforcement is still
permissive, and the conformance matrix shows precisely which rows that buys.
The piece that makes this safe to adopt: a permissive decision is still a
decision, and it gets logged. Before anyone enables enforcement they can read,
from their own daemon, exactly what would have been refused — the dry-run that
auth rollouts normally lack. mcpx can report "17 requests in the last day
would have been refused by scope enforcement, all from your own plugin", which
turns enabling a check from a leap into a verification.
Ship order: (1) split the matrix rows along accepts/refuses and record the split
in the matrix itself, so #253's target is exact; (2) the decision log, while
everything is still permissive, since it has no behavioural risk; (3) checks one
at a time, each defaulting off; (4) flip defaults individually once the log says
it is boring.
Open questions
- Does the permissive interface need to be visibly permissive? A daemon that
says "authorized" to everything should probably say so loudly at startup and
inmcpx doctor, so nobody deploys it believing otherwise. Leaning strongly
yes — this is the one real hazard of the permissive-first approach. - Does the conformance matrix gain a third state? Today a row passes or is
pending:oauth. "Interface present, validation permissive" is honestly a
different state from both, and recording it is what keeps the split legible. - Which checks are even meaningful for a local socket daemon? Audience and
resource binding matter for a remote resource server and are close to
meaningless on a unix socket. The per-check defaults probably differ per
surface. - Do Install seed is readable by every joiner #240 and feat(id): server-wide DHT config flag for serve [P7.3] #205 (configured auth never applied) belong to the permissive
stage or this one? Applying a configured upstream credential is mcpx acting as
a client, which is a different direction from mcpx validating an inbound
one — probably they stay independent of both and are simply bugs. - Is there a conformance row that requires rejection and is cheap? If so it
should be pulled into 🎨 Palette: Add ARIA labels to icon buttons #253 rather than waiting here.
Ease · Impact · Related
Ease: S for the matrix split and the decision log; M per check; L for all of
them. Impact: low until somebody wants a non-local surface, which is the
honest statement — this is conformance completeness rather than a capability.
Blocks nothing.
Depends on / related: #253 (the auth implementation — this is ordered
strictly behind it and should not start until the permissive interface is in),
#240 and #205 (configured auth never applied — mcpx as client, independent of
this), #298 (principals and per-operation permissions — the authorisation model
this would eventually enforce against; also ordered behind #253 and also
blocking nothing), #204 (Origin validation — a separate browser-threat defence,
already shipped-shaped and unrelated to credentials).
Acceptance criteria
- The conformance rows that require refusal (invalid/expired/wrong-audience credentials against the 🎨 Palette: Add ARIA labels to icon buttons #253 interface) are enumerated from the matrix and pass.
- Valid credentials still authorize; the permissive rows from 🎨 Palette: Add ARIA labels to icon buttons #253 do not regress.
- Rejection responses match the spec for status, error shape, and
WWW-Authenticatebehaviour per revision. - A local daemon with no credentials configured keeps today's complete local answer — nobody is locked out of their own daemon by default.
Activity
developing-today commentedon Oct 28, 2026
CODE-897
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#300 (verbatim, oldest first):
@dezren39 · 2026-10-27T14:28:50Z
Intake review
🤖 Agent note · filed
Related: Related: parent #253 (the permissive interface, 61 conformance rows); sibling #298 builds the principal/permission vocabulary behind it. No merged PR explicitly resolves the refusal rows — PR #308's conformance pass predates the interface split this issue describes.
Risks / notes: Behaviour-change risk is the point of the issue: rejection logic can lock a user out of their own daemon. The local-no-credentials path must stay a complete answer by default.
Suggested approach: Enumerate the refusal rows from the conformance matrix first, implement validation behind them one row family at a time, with valid-credential and no-credential paths pinned by tests.
Liveness: searched merged PRs and recent fleet records this run — no merged PR explicitly resolves this issue, so it stays open. Marked
meta/enriched.45 remaining items
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
mcpx: record and report the permissive auth decision (#1972 step 2) (#…
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: done (fixed_merged) — task 8156, resumed and closed out by lane code-b
Step 2 of #1972 (permissive auth decision record + dry-run report) is MERGED: PR #5012 squash-merged at 2026-11-03T16:44:33Z, merge commit 70cf06c.
Verification on resume (fresh, this lane): PR MERGEABLE at head 01fc659, diff = the 8 intended mcpx files; go test ./internal/api/ -count=1 PASS and daemon authz tests (TestAuthzDecisionReportSurface, TestRouteRecordsDecisionBeforeHandlerRuns) PASS on the laptop. The PR's only check (line-limit 2500) never started — Actions run queued since 16:11Z in a fleet-wide runner stall; its substance was verified directly (largest file 1818 lines). Codex review failed on usage limits with zero findings; disposition is on PR #5012 (comment 6609917561).
Issue #1972 stays OPEN: steps 3-4 of its ship order remain. Stale queue/status/work labels from the park cycle removed in this pass.
fix(mcpx): stop judging catalogue-n/a authsplit sides (#2695) (#5138)