Skip to content

Commit 40cf62e

Browse files
MakiDevelopclaude
andcommitted
fix: auth middleware 視空字串 token 為 disabled(對齊 compose env fallback)
commit a995f49 的 bug:docker-compose.yml 寫 `MH_API_TOKEN: ${MH_API_TOKEN:-}`, host env 未設時 compose 展成空字串傳進 container,pydantic 讀 env 把 api_token 設成 ""(不是 None)。middleware 的 `is None` 判斷失效,誤開啟 auth。 今日 deploy 到 mini 時立刻踩到——container 健康但所有 /v1/memory/* 回 401 "missing bearer token",因為 client(skills)還沒帶 header。 修:middleware 改用 `if not active_settings.api_token:`,None 和 "" 都 bypass。 補 test_auth_empty_string_token_also_disables_auth 明確守住這 regression。 ADR-0007 描述的「未設即 bypass」行為靠這個判斷站住。 Directive: 不用 pydantic field_validator 把 "" 轉 None — middleware 一條判斷夠, 加 validator 是多層間接 Rejected: 改 docker-compose.yml 不透傳 MH_API_TOKEN 空值 | 要分情境定義 service env,比 middleware 一個 `not` 判斷複雜得多 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a995f49 commit 40cf62e

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

src/memory_hall/server/app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,8 +655,10 @@ async def require_api_token(request: Request, call_next):
655655
# in-image HEALTHCHECK probe it without credentials.
656656
if request.url.path.rstrip("/") == "/v1/health":
657657
return await call_next(request)
658-
# Backward compat: when api_token is unset, auth is disabled.
659-
if active_settings.api_token is None:
658+
# Backward compat: when api_token is unset (None) or empty string
659+
# (docker-compose `${MH_API_TOKEN:-}` expands to "" when host env is
660+
# unset — pydantic reads that as "", not None), auth is disabled.
661+
if not active_settings.api_token:
660662
return await call_next(request)
661663
header = request.headers.get("authorization", "")
662664
prefix = "Bearer "

tests/test_auth.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ async def test_auth_disabled_allows_unauthenticated_write(tmp_path: Path) -> Non
2727
assert response.status_code in (200, 201, 202)
2828

2929

30+
@pytest.mark.asyncio
31+
async def test_auth_empty_string_token_also_disables_auth(tmp_path: Path) -> None:
32+
# docker-compose `${MH_API_TOKEN:-}` expands to "" when host env is unset.
33+
# pydantic reads that as "" (not None). Middleware must treat "" like None.
34+
settings = build_settings(tmp_path)
35+
settings.api_token = ""
36+
app = create_app(settings=settings, embedder=DeterministicEmbedder(dim=settings.vector_dim))
37+
async with client_for_app(app) as client:
38+
response = await client.post("/v1/memory/write", json=_write_payload())
39+
assert response.status_code in (200, 201, 202)
40+
41+
3042
@pytest.mark.asyncio
3143
async def test_auth_enabled_missing_header_returns_401(tmp_path: Path) -> None:
3244
settings = build_settings(tmp_path)

0 commit comments

Comments
 (0)