## Phase 0(文件層,全部 Accepted) - ADR-106/107:AwoooP 平台架構 + 儲存策略 - ADR-111~118:Bootstrap → RLS 七項核心 ADR - ADR-119~124:SAGA → Singleton Decomposition 六項 ADR - ADR-UI-01~04:Operator Console 四個 UI ADR ## Phase 1(DB schema + migration) - awooop_phase1_control_plane_2026-05-04.sql:7 張新表 + trigger + RLS - Step 1:三角色(platform_admin/migration BYPASSRLS,awooop_app 受 RLS) - Step 13:GRANT awooop_app 最小權限(7 條) - Step 14:RLS fail-closed,移除 __platform__ 後門 - awooop_phase1_batch1_rls_2026-05-04.sql:高流量四表三步式 ADD COLUMN - awooop_phase1_batch1_backfill.py:SKIP LOCKED 分批回填腳本 - awooop_models.py:7 個 SQLAlchemy 2.x models ## Critic 修正(4 Critical + 3 Major) - C-1:ADD CONSTRAINT IF NOT EXISTS → DO 塊 + pg_constraint 查詢 - C-2:__mapper_args__ 字串 list → primary_key=True on mapped_column - C-3:__platform__ RLS 後門 → 全移除,改用 BYPASSRLS role - C-4:awooop_app role 從未建立 → Step 1 + 7 條 GRANT - M-1:active_pointer_guard SECURITY DEFINER(FORCE RLS 跨租戶保護) - M-2:pg_partman create_parent 加冪等防護 - M-3:immutability trigger 新增身份欄位保護(project_id/family/contract_id) ## Task 1.2 修補 - agent_loader.py:硬編碼 Mac 路徑 → AGENTS_DIR 環境變數 - Dockerfile:補 COPY .claude/agents/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
6.2 KiB
Markdown
167 lines
6.2 KiB
Markdown
# 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)
|