flower
/
All briefs
complete draft note flower
epic · Decouple agent/process management from Solo → flower...

flower /mcp HTTP auth — config-gated bearer middleware

canonical · plan

Spec

markdown

hand-off · dispatch

Dispatch

Auto-dispatch

when it reaches planned

Design-loop

design pass before build

This brief is complete — dispatch is closed.

#144 done fresh flower
claimed by flower-277-worker
You are being dispatched from flower Brief #277: flower /mcp HTTP auth — config-gated bearer middleware

Recall pointer:
- Use recall_brief with id 277 for the full folder if you need provenance.

Target:
- project: flower (/Users/mikeferrara/Documents/code/flower)
- branch: choose an appropriate branch
- worktree: not specified
- kind: fresh

Current brief spec:
# flower /mcp HTTP auth — config-gated bearer middleware

**Overnight auto-dispatch, review-gated.** Parent epic #263. Closes the unauthenticated-`/mcp` gap flagged during tonight's remote-worker build.

## Problem
`Mcp::web('/mcp', FlowerServer::class)` (routes/ai.php:17) is served with **no auth** — the code comment literally calls auth "a future middleware/auth seam." Remote flower-agents reach it over the tailnet; without auth, any tailnet (or wider, if exposed) client can call flower's full write surface.

## Goal
A **config-gated bearer-token middleware** on the `/mcp` HTTP route: enforce when a token is configured, backward-compatible (open) when not, and **leave the stdio server untouched**.

## Spec
1. **Config:** add `flower.mcp.http_token` (env `FLOWER_MCP_HTTP_TOKEN`, default null/empty) in config/flower.php.
2. **Middleware:** only when `flower.mcp.http_token` is non-empty, require `Authorization: Bearer <token>` matching it via constant-time `hash_equals`; return **401** on missing/mismatch. When the config token is empty → pass through (today's behavior; local dev unbroken).
3. **HTTP route only:** attach it to `Mcp::web('/mcp', …)` in routes/ai.php. **Do NOT touch `Mcp::local('flower', …)`** (stdio; used by the daemons + interactive sessions — must keep working with zero change).

## Acceptance
- `php artisan test` green, with new tests: (a) token set + no/incorrect bearer → 401; (b) token set + correct bearer → normal MCP response; (c) token unset → open (backward-compat); (d) stdio flower MCP unaffected.
- `./vendor/bin/pint` clean on changed files.

## Non-goals
- `tailscale serve` exposure of flower.test (brief #110 — separate).
- Per-user/per-request identity (single shared token is fine for v1).

## Notes
- The remote `flower-agent` wrapper (bin/flower-agent, committed tonight) already sends `Authorization: Bearer $FLOWER_MCP_TOKEN` when set — so enabling later = set `FLOWER_MCP_HTTP_TOKEN` on the server + the matching token in the worker env. This brief is the server half.
- **Review-gated:** mandatory adversarial review before merge (security-sensitive).

Recent/key trace events:
[1] participant_joined claude-interactive: (no body)
[2] note_added claude-interactive: Add auth to flower's HTTP /mcp route (Mcp::web('/mcp', FlowerServer::class), routes/ai.php) — a config-gated bearer-token middleware (FLOWER_MCP_HTTP_TOKEN): enforce a 401 when the token is set, backward-compatible (open) when unset so local dev isn't broken; the stdio Mcp::local('flower') server (used by daemons + interactive sessions) stays UNTOUCHED. Closes the unauthenticated-/mcp gap (flagged tonight) and lets the remote flower-agent wrapper (already supports FLOWER_MCP_TOKEN) authenticate over the tailnet. Overnight auto-dispatch, review-gated. Full spec to follow.
[3] plan_proposed claude-interactive: # flower /mcp HTTP auth — config-gated bearer middleware

**Overnight auto-dispatch, review-gated.** Parent epic #263. Closes the unauthenticated-`/mcp` gap flagged during tonight's remote-worker build.

## Problem
`Mcp::web('/mcp', FlowerServer::class)` (routes/ai.php:17) is served with **no auth** — the code comment literally calls auth "a future middleware/auth seam." Remote flower-agents reach it over the tailnet; without auth, any tailnet (or wider, if exposed) client can call flower's full write surface.

## Goal
A **config-gated bearer-token middleware** on the `/mcp` HTTP route: enforce when a token is configured, backward-compatible (open) when not, and **leave the stdio server untouched**.

## Spec
1. **Config:** add `flower.mcp.http_token` (env `FLOWER_MCP_HTTP_TOKEN`, default null/empty) in config/flower.php.
2. **Middleware:** only when `flower.mcp.http_token` is non-empty, require `Authorization: Bearer <token>` matching it via constant-time `hash_equals`; return **401** on missing/mismatch. When the config token is empty → pass through (today's behavior; local dev unbroken).
3. **HTTP route only:** attach it to `Mcp::web('/mcp', …)` in routes/ai.php. **Do NOT touch `Mcp::local('flower', …)`** (stdio; used by the daemons + interactive sessions — must keep working with zero change).

## Acceptance
- `php artisan test` green, with new tests: (a) token set + no/incorrect bearer → 401; (b) token set + correct bearer → normal MCP response; (c) token unset → open (backward-compat); (d) stdio flower MCP unaffected.
- `./vendor/bin/pint` clean on changed files.

## Non-goals
- `tailscale serve` exposure of flower.test (brief #110 — separate).
- Per-user/per-request identity (single shared token is fine for v1).

## Notes
- The remote `flower-agent` wrapper (bin/flower-agent, committed tonight) already sends `Authorization: Bearer $FLOWER_MCP_TOKEN` when set — so enabling later = set `FLOWER_MCP_HTTP_TOKEN` on the server + the matching token in the worker env. This brief is the server half.
- **Review-gated:** mandatory adversarial review before merge (security-sensitive).
[4] parent_set claude-interactive: Grouped under epic #263.
[5] review_requested claude-interactive: Security-sensitive: verify the bearer middleware is applied ONLY to the Mcp::web('/mcp') HTTP route (stdio Mcp::local untouched), is config-gated (open when FLOWER_MCP_HTTP_TOKEN unset — backward-compatible), uses constant-time comparison, and that tests cover the 401 / correct-bearer / unset / stdio-unaffected cases. Do not merge if local/daemon flower MCP usage could break.
[6] status_change claude-interactive: (no body)

Recommended linked context:
{
    "todos": [],
    "scratchpads": []
}

Execution notes:
- Treat the brief as the source of truth.
- Keep work scoped to this dispatch request.
- Use brief_append / brief_update_status when reporting material progress; as your final dispatched-worker step, call brief_dispatch_complete with dispatch_request_id (or brief_id) and actor_ref.
- Codex workers should verify mutating Flower tools with tool_search query `brief_append brief_dispatch_complete flower_feedback` (limit 20) when tool availability is in doubt; report raw SEE/LOAD vs NOT visible instead of silently using local fallbacks.
- Add a git commit trailer `Brief: #277` to every commit for this brief so flower can exact-link commits back to the brief.
- Need an operator call while working this brief? A question ABOUT THIS BRIEF -> brief_ask(277, ...); a standalone decision not tied to the brief -> decision_ask(...). Both expose the full affordance set (confirm | single_choice | multi_choice | text, options + recommended, allow_write_in); prefer async questions over blocking and set is_blocking only when you truly cannot proceed.
- Cited-refs index (Brief #244): when a report / checkpoint / DONE summary cites numbered entities, append a compact `Refs:` block at the END mapping each `#N` to its REAL stored title + status — `#<num>: <title> (<status>)`, one per line (typed prefixes: bare `#N` = brief, `fb#N` feedback, `dec#N` decision, `sess#N` session, `dr#N` dispatch_request). Resolve any title you did not already pull with recall_refs instead of guessing; skip the block on trivial one-liners.

provenance · append-only

Trace

live
or paste a screenshot uploading…
  1. link added 9h ago
    agent · system:commit-trailer
  2. link added 9h ago
    agent · system:commit-trailer
  3. participant joined 9h ago
    system · system:commit-trailer
  4. status change 10h ago
    agent · flower-orchestrator
  5. review passed 10h ago

    PASS — Security review PASS: bearer middleware attached ONLY to Mcp::web('/mcp') (stdio Mcp::local untouched); config-gated (empty FLOWER_MCP_HTTP_TOKEN → open, backward-compatible); constant-time hash_equals with correct null-guard; 401 on missing/mismatch. All 5 tests green (no-bearer/wrong→401, correct→200, unset→open, stdio-unaffected). No data migration; safe to merge (no-op until token set).

    agent · flower-orchestrator
  6. status change 10h ago
    agent · flower-277-worker
  7. dispatched 10h ago

    Dispatch request #144 marked done.

    agent · flower-277-worker
  8. note added 10h ago

    Built + committed on flower/277-mcp-http-auth (commit f2f9b8f, `Brief: #277` trailer). **What shipped** - `config/flower.php`: added `flower.mcp.http_token` = `env('FLOWER_MCP_HTTP_TOKEN')` (default null) inside the existing `mcp` block. - `app/Http/Middleware/AuthenticateMcpHttp.php` (new): config-gated. Reads `flower.mcp.http_token`; empty/whitespace → `$next()` (open, today's behavior). Non-empty → require `Authorization: Bearer <token>` compared via constant-time `hash_equals`; 401 JSON on missing/mismatch. Provided token is null-guarded before hash_equals (it TypeErrors on null). - `routes/ai.php`: attached `->middleware(AuthenticateMcpHttp::class)` to the HTTP `Mcp::web('/mcp')` route ONLY. `Mcp::local('flower', …)` (stdio) is byte-for-byte unchanged. Updated the stale "future middleware/auth seam" comment. - `tests/Feature/Mcp/McpHttpAuthTest.php` (new, 5 tests): (a) token set + no bearer → 401; token set + wrong bearer → 401; (b) token set + correct bearer → 200 MCP ping response; (c) token unset AND whitespace-only → open (backward-compat); (d) stdio path unaffected — `FlowerServer::tool(RecallOpenLoopsTool)` still OK with a token configured. **Status:** `MEILISEARCH_KEY=LARAVEL-HERD ANTHROPIC_API_KEY= ~/bin/php artisan test` GREEN — 1281 tests, 1278 passed, 3 pre-existing skips. `~/bin/php ./vendor/bin/pint` clean on all changed files. No migrations. `_dispatch-277.md` NOT committed. Did not merge. **Security notes for the adversarial reviewer** - Constant-time compare via `hash_equals($configToken, $providedToken)` — known-string first, user-string second; only the provided-token LENGTH can leak (standard), never content. - Null/absent bearer short-circuits to 401 before hash_equals — this only reveals header presence (attacker-controlled), not token bytes. - Middleware sits on the POST route only (the Route that `Mcp::web` returns). GET/DELETE `/mcp` return 405 (Allow: POST) WITHOUT invoking FlowerServer, so leaving them unauthenticated exposes no data/write surface. - The 401 also receives a spec-compliant `WWW-Authenticate: Bearer realm="mcp", error="invalid_token"` header — free, because the package's `AddWwwAuthenticateHeader` middleware runs earlier in the stack and my middleware runs after it. - Config token is `trim()`-ed so a whitespace-only env value counts as "unset" (open) rather than a near-open lock; the client-provided token is NOT trimmed (exact match preserved). No token value is logged. - phpunit.xml sets no `FLOWER_MCP_HTTP_TOKEN`, so the whole existing suite runs in the open/backward-compat path — nothing else changed. - Enable in prod = set `FLOWER_MCP_HTTP_TOKEN` on the server + the matching `FLOWER_MCP_TOKEN` in the remote flower-agent worker env (the wrapper already sends `Authorization: Bearer $FLOWER_MCP_TOKEN`).

    agent · flower-277-worker
  9. status change 10h ago
    agent · flower-277-worker
  10. link added 10h ago
    agent · flower-277-worker
  11. link added 10h ago
    agent · flower-277-worker
  12. dispatched 10h ago

    Dispatch request #144 claimed.

    agent · flower-277-worker
  13. participant joined 10h ago
    system · flower-277-worker
  14. dispatched 10h ago

    Dispatch request #144 queued for flower.

    agent · flower-orchestrator
  15. status change 10h ago
    agent · flower-orchestrator
  16. participant joined 10h ago
    system · flower-orchestrator
  17. status change 10h ago
    agent · claude-interactive
  18. review requested 10h ago

    Security-sensitive: verify the bearer middleware is applied ONLY to the Mcp::web('/mcp') HTTP route (stdio Mcp::local untouched), is config-gated (open when FLOWER_MCP_HTTP_TOKEN unset — backward-compatible), uses constant-time comparison, and that tests cover the 401 / correct-bearer / unset / stdio-unaffected cases. Do not merge if local/daemon flower MCP usage could break.

    agent · claude-interactive
  19. parent set 10h ago

    Grouped under epic #263.

    agent · claude-interactive
  20. plan proposed 10h ago

    # flower /mcp HTTP auth — config-gated bearer middleware **Overnight auto-dispatch, review-gated.** Parent epic #263. Closes the unauthenticated-`/mcp` gap flagged during tonight's remote-worker build. ## Problem `Mcp::web('/mcp', FlowerServer::class)` (routes/ai.php:17) is served with **no auth** — the code comment literally calls auth "a future middleware/auth seam." Remote flower-agents reach it over the tailnet; without auth, any tailnet (or wider, if exposed) client can call flower's full write surface. ## Goal A **config-gated bearer-token middleware** on the `/mcp` HTTP route: enforce when a token is configured, backward-compatible (open) when not, and **leave the stdio server untouched**. ## Spec 1. **Config:** add `flower.mcp.http_token` (env `FLOWER_MCP_HTTP_TOKEN`, default null/empty) in config/flower.php. 2. **Middleware:** only when `flower.mcp.http_token` is non-empty, require `Authorization: Bearer <token>` matching it via constant-time `hash_equals`; return **401** on missing/mismatch. When the config token is empty → pass through (today's behavior; local dev unbroken). 3. **HTTP route only:** attach it to `Mcp::web('/mcp', …)` in routes/ai.php. **Do NOT touch `Mcp::local('flower', …)`** (stdio; used by the daemons + interactive sessions — must keep working with zero change). ## Acceptance - `php artisan test` green, with new tests: (a) token set + no/incorrect bearer → 401; (b) token set + correct bearer → normal MCP response; (c) token unset → open (backward-compat); (d) stdio flower MCP unaffected. - `./vendor/bin/pint` clean on changed files. ## Non-goals - `tailscale serve` exposure of flower.test (brief #110 — separate). - Per-user/per-request identity (single shared token is fine for v1). ## Notes - The remote `flower-agent` wrapper (bin/flower-agent, committed tonight) already sends `Authorization: Bearer $FLOWER_MCP_TOKEN` when set — so enabling later = set `FLOWER_MCP_HTTP_TOKEN` on the server + the matching token in the worker env. This brief is the server half. - **Review-gated:** mandatory adversarial review before merge (security-sensitive).

    agent · claude-interactive
  21. note added 10h ago

    Add auth to flower's HTTP /mcp route (Mcp::web('/mcp', FlowerServer::class), routes/ai.php) — a config-gated bearer-token middleware (FLOWER_MCP_HTTP_TOKEN): enforce a 401 when the token is set, backward-compatible (open) when unset so local dev isn't broken; the stdio Mcp::local('flower') server (used by daemons + interactive sessions) stays UNTOUCHED. Closes the unauthenticated-/mcp gap (flagged tonight) and lets the remote flower-agent wrapper (already supports FLOWER_MCP_TOKEN) authenticate over the tailnet. Overnight auto-dispatch, review-gated. Full spec to follow.

    agent · claude-interactive
  22. participant joined 10h ago
    system · claude-interactive

epic · dependencies

Relationships

depends on

No dependencies — dispatchable once planned.

agents · waves

Participants

  • claude-interactive participant · active
  • flower-orchestrator reviewer · active
  • flower-277-worker participant · active
  • system:commit-trailer participant · active

trace · graph

Links

  • Commit #4085 execution
  • Commit #4086 execution
  • Session #3545 execution
  • dispatch_request #144 execution

scope

Projects

  • flower · primary

dogfood · read-only

Agent’s-eye view

The literal recall_brief payload an agent gets — same service path as the MCP tool.