Introduce two-stage local control auth, stage 6a: accept-and-log (rebuild W3 PR-G)#144
Merged
Conversation
…uild W3 PR-G)
First half of the W3-6 auth plan: the installation capability exists and is
attached everywhere, but nothing is enforced — missing or invalid auth logs a
rate-limited WARN and passes through. Enforcement (401, stage 6b) ships one
packaged-app release later, after the Swift wrapper attaches the token, so no
unauthenticated native call or MCP client breaks today.
- internal/web/auth.go: 256-bit control token minted-or-loaded at
<dataDir>/control.token (0600). Minting writes a temp file and renames
atomically, and an empty or undecodable existing file is regenerated with a
WARN instead of failing startup — a partial write (e.g. disk full during
the one-time mint) previously left a file that bricked every subsequent
start, denying all callers through the side door the accept-and-log
contract exists to prevent. Constant-time compare; token never logged.
- Requests authenticate via Authorization: Bearer or the om_control cookie
(HttpOnly, SameSite=Strict, Path=/). A single-use loopback bootstrap URL
(printed once to stderr by serve --web) sets the cookie and redirects; the
real token never appears in a URL, wrong guesses never consume the code,
reuse gets 410, and the endpoint requires a loopback Host before
evaluating anything.
- Middleware inside ProtectLocalControl classifies bearer/cookie/missing/
invalid, WARNs once per remote host (port-stripped; map capped at 1024
with clear-and-warn overflow), and always passes through. /api/status
gains auth: {token_present, mode: "accept-and-log", data_dir}.
- CLI sends resolve the token from the DAEMON's data dir via the status
probe rather than the CLI default — the macOS app runs the server against
~/Library/Application Support/OpenMessage while a bare terminal defaults
to ~/.local/share/openmessage, so local-default resolution would attach
nothing and break openmessage send the day 6b enforces. The 0600 file
remains the capability; only the path travels over the status endpoint.
- Riding 6a per spec: Cross-Origin-Resource-Policy: same-origin on /api/*
and /mcp/* (UI media loads are same-origin relative URLs; bootstrap
excluded), Sec-Fetch-Site anomalies logged never rejected, SSE subscribers
capped at 32 (frontend falls back to polling with backoff on 503) with a
write deadline on every SSE event so half-open sockets cannot hold a slot
for the TCP retransmit timeout.
- Swift header attach is deliberately a separate PR (spec: R6-adjacent).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuild W3 PR-G — auth stage 6a (accept-and-log, never reject)
Final Wave-3 lane. The installation capability (
control.token, cookie, bearer) now exists end-to-end, with zero enforcement: missing/invalid auth WARNs once per remote and passes through. 401s are stage 6b, one packaged-app release later, after the Swift wrapper attaches the token — so the live v2-primary install keeps working untouched.Review
Opus adversarial review: FIX-FIRST → all 6 findings fixed with discriminators. The two that mattered:
control.tokenthat bricked every subsequent server start — the exact denial the 6a contract forbids, via file robustness instead of the auth path. Now: atomic temp+rename mint, and empty/undecodable existing files regenerate with a WARN instead of failing startup.~/Library/Application Support/OpenMessage→ nothing attached →openmessage sendwould 401 the day 6b lands. Now: the CLI resolves the token path from the daemon's own/api/status auth.data_dir(server-truth, like PR-E's mode probe); the 0600 file remains the capability — only the path travels over HTTP.Plus: de-vacuized the once-per-remote WARN test (same host twice → exactly one WARN), SSE write deadlines so half-open sockets can't hold subscriber slots, warned-map bounded at 1024, bootstrap loopback-gated (defense in depth; wrong guesses never consumed the code even before).
Verification
GOWORK=off go test -race ./...outside the sandbox: 32/32 packages, exit 0🤖 Generated with Claude Code