# 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）
