Skip to content

D45 — Per-KB r/rw scope enforcement: scoped TokenStore + body-peek HTTP guard + fail-closed read-only classification

Decision. Connects runtime enforcement to the D44 config plumbing. auth.TokenStore moves to map[string][]KBScope (NewScopedTokenStore, backward compatible with NewTokenStore). Per-tool enforcement lives in the HTTP guard (mcpAccessGuard), not in the Middleware: if the token has scopes, the guard reads the JSON-RPC body (io.LimitReader, 2MB) and always restores it onto r.Body (the downstream handler re-reads from scratch), determines needWrite from ToolRequiresWrite(tool) (fail-closed on unparsable JSON or unknown tool) and checks auth.HasAccess. New field Tool.ReadOnly bool marks tools that never mutate the KB; ToolRequiresWrite consults a dedicated map (readOnlyToolNames), verified against the real registry by a golden test (TestReadOnlyToolsGolden) to avoid silent divergences. Rationale. Fail-closed on both axes (unknown tool → write; scope with no match → 403) because it is security-sensitive code: better one 403 too many than a silent bypass. The body restore is tested explicitly because without it every scope-authenticated call would silently break. Details: docs/transport-auth.md §Per-KB authorization, docs/control-plane.md §Read/write boundary.