# ADR-111: AwoooP Bootstrap Order & Identity Paradox **狀態**:Accepted **日期**:2026-05-03(台北) **決策者**:統帥 **範圍**:AwoooP 平台啟動順序、project_id 標記體系、31 個 background loop 的 identity 策略 **關聯**:ADR-106(架構)、ADR-107(儲存)、ADR-123(background loop migration) --- ## 背景 AWOOOI 作為 AwoooP 的 first tenant 和 first runtime host,在系統啟動時存在一個根本矛盾: - AwoooP 的核心原則是「所有操作必須帶 project_id」 - 但 `apps/api/src/main.py` 有 **31 個 background loop**(onboarder 實測),全部都沒有 project_id - AWOOOI 既是「需要被 project_id 保護的 tenant」,又是「在任何 project 被建立前就要運行的平台基礎設施」 這就是 **Bootstrap Paradox**:你需要平台先啟動,才能建立 project;但啟動時又沒有 project。 --- ## 問題清單 1. 31 個 background loop 無 project_id(main.py 實測) 2. AWOOOI cron/job/healthcheck 在 AwoooP 對象化之前就必須運行 3. `run_ai_slo_watchdog_loop()` 是平台自我監控,不屬於任何 tenant 4. `_run_model_version_tracker_loop()` 追蹤所有 tenant 共用的 AI provider 版本,屬於 platform resource 5. GCP Ollama 三層容災(ADR-110)的 failover 狀態(`ollama:current_primary`)是 platform_resource,不屬於任何 tenant --- ## 決策 ### D1 — 三種 project_id 標記 所有入口點(loop、webhook、job、CLI)必須帶其中一個標記: | 標記 | project_id 值 | 適用情境 | |------|-------------|---------| | `platform_internal` | `__platform__` | 平台自我維護、不屬於任何 tenant;允許讀取 platform_resource,但必須記 audit | | `legacy_awoooi_default` | `awoooi` | 過渡期:AWOOOI 業務 loop 尚未改造前的臨時 fallback | | `requires_project_id` | 從呼叫鏈或 event envelope 取得 | 已完成多租戶改造的入口點 | ### D2 — Platform Resource 明確清單 以下 Redis key 和狀態是 **platform_resource**,使用前綴 `platform:` 而非 `{project_id}:`: | Resource | Key | 理由 | |----------|-----|------| | Ollama topology | `platform:ollama:topology` | GCP-A/GCP-B/Local 三層路由,所有 tenant 共用(ADR-110)| | Telegram polling leader | `platform:telegram:polling:leader` | 全域 pod 鎖,不屬於任何 tenant | | Gemini daily count | `platform:ollama:gemini_daily_count:{date}` | 全平台 Gemini 緊急路由計數 | | SLO watchdog state | `platform:slo_watchdog:*` | 平台自我監控 | | Model version cache | `platform:model_version:*` | 所有 tenant 共用的 provider 版本資訊 | ### D3 — 啟動順序(Hard Order) ``` 1. DB migrations(awooop_projects + awooop_contract_revisions 等) 2. Seed AWOOOI project_id(INSERT IF NOT EXISTS) 3. platform_internal services 啟動(SLO watchdog、model version tracker) 4. legacy_awoooi_default services 啟動(AWOOOI 業務 loop,帶 project_id=awoooi) 5. API server 開始接收請求 6. requires_project_id services 啟動(Phase 2+ 改造後的新入口點) ``` **禁止**:API server 在 step 2(seed AWOOOI project)完成前接受任何需要 project_id 的請求。 **實作**:FastAPI lifespan context manager 控制啟動順序。 ### D4 — 過渡期豁免規則(legacy_awoooi_default) 以下條件下,`project_id=awoooi` 作為過渡期 fallback 是允許的: - 明確標記為 `legacy_awoooi_default`(不能靜默 fallback) - 寫入 audit log(標記 `legacy=true`) - 有退場時程(Phase 4 完成後 90 天內改造完畢) ### D5 — Hard Reject 規則 在 Phase 2 完成後,以下情況**必須拒絕**(不能靜默 fallback): - 新建立的 API endpoint 收到無 project_id 的請求(非 legacy) - `requires_project_id` 標記的 loop 缺少 project_id 的 context variable ### D6 — GCP Ollama 拓撲的 bootstrap 處理 ADR-110 的三層 Ollama 拓撲是 platform_resource: - bootstrap 時必須在 step 3 之前確認 GCP-A/GCP-B 健康狀態 - `platform:ollama:topology` Redis key 由 `ollama_failover_manager.py` 在啟動時初始化 - 所有 tenant 的 LLM call 使用這個 platform-level topology,不能 per-project 覆蓋(safety: no per-tenant Ollama endpoint override) --- ## 實作指引 ### Python contextvars 實作 ```python # apps/api/src/core/context.py(新建) import contextvars project_id_ctx_var: contextvars.ContextVar[str] = contextvars.ContextVar( 'project_id', default='__platform__' ) def get_current_project_id() -> str: return project_id_ctx_var.get() async def run_with_project(coro, project_id: str): """在指定 project context 中執行 coroutine""" token = project_id_ctx_var.set(project_id) try: return await coro finally: project_id_ctx_var.reset(token) ``` ### main.py 改造示意(Phase 2 完成後) ```python # platform_internal loop asyncio.create_task(run_with_project( run_ai_slo_watchdog_loop(), "__platform__" )) # legacy_awoooi_default loop(過渡期) asyncio.create_task(run_with_project( run_incident_analysis_sweeper(), "awoooi" )) ``` --- ## 後果 ### Benefits - Bootstrap Paradox 有明確解法(三種標記 + 啟動順序) - platform_resource 明確隔離,不會和 tenant 資料混算 - GCP Ollama 三層拓撲作為 platform_resource,不受 per-tenant policy 影響 ### Costs - 31 個 background loop 全部需要改造(INV-8 列出優先序) - main.py 啟動邏輯需要重構(step 1~6 排序) - 需要新建 `apps/api/src/core/context.py` ### Risks - legacy_awoooi_default 標記若不退場,會永遠留在 codebase - 緩解:ADR-123 明確定義退場時程,Phase 4 後 90 天 --- ## 驗收標準 - [ ] `apps/api/src/core/context.py` 建立(project_id contextvars) - [ ] main.py 31 個 loop 全部帶 project_id context(PR-10 完成) - [ ] `platform_internal` loop 帶 `project_id=__platform__` - [ ] `legacy_awoooi_default` loop 帶 `project_id=awoooi` - [ ] 啟動順序 D3 被 FastAPI lifespan 強制執行 - [ ] `platform:ollama:topology` key 在啟動時正確初始化 ## 關聯 - ADR-106(架構)、ADR-107(儲存)、ADR-110(GCP Ollama 拓撲) - ADR-123(Background Loop Migration Strategy,配套 ADR) - INV-3(Entrypoints)、INV-8(Background Loop Catalog)