Skip to content

mcpx: a principal per surface and a permission per operation, declared permissively #1968

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#298 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

The daemon has no notion of who is calling. That is deliberate and documented
three times over — "the API is unauthenticated, so the network it is on is the
access control" (internal/daemon/server.go:214), "the socket's file
permissions are the access control" (internal/daemon/origin.go:13), "Every
endpoint is unauthenticated" (internal/api/openapi.go:40) — and for a daemon
on a unix socket it is the right model.

This issue is the authorisation model: one notion of a principal that each
surface can construct, and a permission declared on each operation. Built
permissively, in the same spirit as #253: every principal resolves to the owner,
every permission check answers yes, and the daemon behaves exactly as it does
today. What the work buys is that the vocabulary exists and is declared in one
place, so a future surface has something to populate and a future policy has
something to constrain.

This blocks nothing. No surface, transport or feature waits on it. An
unauthenticated local daemon is a complete answer and stays one. Ordered behind
#253 so the auth work proceeds in one line rather than two.

Original notes

security is just off the table right now, there's no blocker for adding
security, there's no blocker for not having auth. just dont mix auth with
other things in issues

Raised after an earlier draft of this issue framed itself as a precondition for
remote surfaces. It is not one, and the framing was wrong.

Current state

Unauthenticated by design, and consistently so. Three separate places state
the model in the same terms, which indicates a decision rather than a drift. The
TCP listener's host is deliberately not defaulted
(internal/daemon/server.go:216-220): "that has to be somebody's decision
rather than a default." The daemon will bind a port and declines to guess you
meant to expose it.

The peer uid is available and unread. A unix socket connection carries peer
credentials (SO_PEERCRED / LOCAL_PEERCRED). mcpx never looks. "Which local
user is calling" is a principal obtainable today for free.

The ops table is the asset. Every operation is declared once in
internal/api/ops*.go, and that declaration drives the CLI command, the MCP
tool and the OpenAPI operation. #114 names this as the project's own precedent —
"one declaration should drive every surface". A permission attached there is
enforced everywhere by construction, including on surfaces that do not exist
yet.

What is adjacent and is not this. #253 is the authorization interface
(OAuth, resource server, 61 conformance rows). #300 is real credential
validation, behind #253. #240 and #205 are configured upstream auth never being
applied — mcpx as a client, the opposite direction. #204 is Origin validation,
a browser-threat defence, not authentication. #185 is a settings ceiling a
caller may lower but not raise — the same shape as a permission and worth
sharing machinery with.

Approaches

Approach A — least recommended: a principal only when a surface needs one

Leave it until ssh or a browser surface lands, then add identity to that
surface.

This is how a codebase ends with four authentication systems: each surface
invents a principal because none existed when it was written. The cost of
declaring the vocabulary now is small precisely because nothing depends on it
— there is no migration, no compatibility surface and no behaviour to preserve.
Doing it later means doing it four times, against live surfaces.

Approach B — middle: principals now, permissions when needed

Construct a principal per surface, carry it through, and defer the permission
declarations.

Half the value for most of the work. A principal nothing consults is a field
that rots; without a permission to check it against, nothing exercises it and it
will be wrong by the time it matters.

Approach C — recommended: both, permissively, and a decision that is always "yes" but always recorded

1. Every surface constructs a principal. One internal notion, built from
what each transport natively knows:

surface principal from
unix socket peer credentials — the uid, available today, read nowhere
loopback TCP nothing native; explicit configuration, or anonymous
ssh (#85) the authenticated key
browser (#174) a session, alongside #204's Origin check
plugin (#82) a declared plugin identity rather than ambient trust

2. Every operation declares a permission. A field on the ops-table entry,
checked at the one place all surfaces funnel through. Default policy: the
socket's owner gets everything, anonymous gets everything, and the check
always returns yes
. Behaviour is identical to today.

3. The decision is recorded even though it is always yes. This is the piece
that makes the permissive stage worth shipping rather than merely harmless.
With principals constructed and permissions declared, mcpx can answer "what
would a policy have refused?" from real traffic, before any policy exists. That
is the same dry-run property #300 wants for credential checks, and the two
should share one mechanism.

What this deliberately does not do: define a policy file format, decide
which scopes may grant, or enforce anything. Those are a later issue, and they
are the parts with actual risk.

Open questions

Ease · Impact · Related

Ease: M for principals and the declaration field; S for the permissive check
and the decision log. The enforcement, policy format and config-chain
constraints are explicitly out of scope and belong to a later issue.
Impact: low and deferred — this is vocabulary, not capability. Worth doing
early only because it is cheap while nothing depends on it. Blocks nothing.

Depends on / related: #253 (the auth interface — this is ordered behind
it)
, #300 (real credential validation — the sibling second stage, also behind
#253, also blocking nothing), #185 (clamped settings — same ceiling shape,
share machinery), #204 (Origin validation — adjacent, not a substitute), #240
and #205 (configured upstream auth never applied — mcpx as client, the other
direction), #255 (elicitation storage permissions — a case a principal would
make expressible), #82 (plugins as a principal), #114 (tool views as a possible
grant unit), #113 (the operation surface where a policy would be administered),
#174 (surfaces — each would supply a principal as it lands, none waits on this).

Acceptance criteria

  • A principal type exists that every surface (unix socket, TCP, HTTP) can construct, and a permission is declared per operation in one place.
  • Built permissively: every principal resolves to the owner and every check answers yes — daemon behaviour is byte-for-byte unchanged for existing clients.
  • The vocabulary is documented where the current unauthenticated model is stated (server.go, origin.go, openapi.go), superseding those comments.
  • Tests pin the permissive behaviour so a future policy change is a deliberate diff, not an accident.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#298 (verbatim, oldest first):


    @dezren39 · 2026-10-27T14:29:24Z

    Intake review

    🤖 Agent note · filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-27T14:14Z
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: intake pass — liveness check, labels normalized, acceptance criteria appended to the body
    • Expectation: see the effort label on this issue
    • Links: n/a

    Related: Related: parent #253; sibling #300 is the refusal-behaviour half. The body's own framing is binding: this blocks nothing and must not be mixed with surface/transport work. No merged PR resolves it.

    Risks / notes: Vocabulary-only change with permissive semantics — the main risk is scope creep into actual policy, which the issue explicitly forbids. Keep the diff to types, declarations, and documentation of the model.

    Suggested approach: Introduce the principal type + per-operation permission declarations in one pass over the three places the unauthenticated model is currently stated, with permissive-behaviour 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.

  3. added
    kind/securitySecurity issue or security hardening
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/mMedium: expected under 1 day of work
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked-up

  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  6. 57 remaining items

  7. added and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: task 7818
    • Found via: monitor reconciliation
    • Task: Claim-eviction repair: the queue task for this issue is queued and unclaimed (its previous claim's lease expired without a complete/park/block), but the issue labels still said claimed. Labels reset to the truth: queue/queued + status/needs-triage. Checkpoint stage on the task: handed_off.
    • Expectation: a queue lane re-claims the task and resumes from the checkpoint stage named in the task progress.
  9. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (already handled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/mMedium: expected under 1 day of workkind/securitySecurity issue or security hardeningmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/humanFiled directly by a humantask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions