soul.demarkus.io:6309/plans/read-auth.md/v1 draft reader meta

Read Auth for Private Networks

Design

Model: If any token in the store has "read" in its operations AND its path pattern matches the request path, that path requires read auth. If no such token exists, the path is public. No new config — the existing tokens TOML file drives everything.

Performance: Pre-compute readPaths []string at load time by collecting path patterns from tokens with "read" in operations. RequiresReadAuth calls matchesAnyPath(ts.readPaths, reqPath) — no iteration over tokens at request time.

Exempt paths: /health and /.well-known/agent-manifest.md bypass read auth so agents can discover capabilities before authenticating.

Content-addressed fetch: Hash lookups resolve to a real path first, then read auth checks that path — knowing a hash doesn't bypass access control.

Changes

1. server/internal/auth/auth.go

Add readPaths []string field to TokenStore. Populate in LoadTokens and NewTokenStore by collecting paths from tokens with "read" in operations.

type TokenStore struct {
    tokens    map[string]Token
    readPaths []string         // pre-computed from tokens with "read" op
    now       func() time.Time
}

func (ts *TokenStore) RequiresReadAuth(reqPath string) bool {
    return matchesAnyPath(ts.readPaths, reqPath)
}

2. server/internal/auth/auth_test.go

Table-driven tests for RequiresReadAuth:

  • Path covered by a read token → true
  • Path NOT covered by any read token → false
  • Path covered by a publish-only token → false
  • No tokens at all → false
  • Glob patterns work (/private/** covers /private/doc.md)

3. server/internal/handler/handler.go

Add helper:

func (h *Handler) authorizeRead(w io.Writer, req protocol.Request) bool

Logic:

  1. Get token store. If nil → return true (public).
  2. If req.Path is protocol.WellKnownManifestPath → return true (always public).
  3. ts.RequiresReadAuth(req.Path) — if false → return true (public path).
  4. ts.Authorize(req.Metadata["auth"], req.Path, "read") — if error → write error, return false.
  5. Return true.

Call at the top of handleFetch, handleList, handleVersions.

For handleFetchByHash: check read auth on the resolved path after hash lookup, before serving.

4. server/internal/handler/handler_test.go

Table-driven tests:

  • FETCH protected path without token → unauthorized
  • FETCH protected path with valid read token → ok
  • FETCH protected path with wrong token → unauthorized
  • FETCH public path without token → ok
  • LIST protected path without token → unauthorized
  • LIST protected path with valid token → ok
  • VERSIONS protected path without token → unauthorized
  • VERSIONS protected path with valid token → ok
  • FETCH by hash where resolved path is protected → unauthorized without token
  • FETCH by hash where resolved path is protected → ok with token
  • FETCH /.well-known/agent-manifest.md → ok without token on protected server
  • No token store configured → all reads public

Order of Work

  1. readPaths field + RequiresReadAuth + tests in auth package
  2. authorizeRead helper + integration into handler
  3. Handler tests
  4. bash pre-commit.sh
trail
  1. soul.demarkus.io:6309 v1