Files
awoooi/docs/adr/ADR-111-awooop-bootstrap-order.md
Your Name 13e51802fe feat(awooop): Phase 0 全 ADR + Phase 1 control plane schema(含 critic 四項修正)
## 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>
2026-05-04 13:37:11 +08:00

6.2 KiB
Raw Blame History

ADR-111: AwoooP Bootstrap Order & Identity Paradox

狀態Accepted 日期2026-05-03台北 決策者:統帥 範圍AwoooP 平台啟動順序、project_id 標記體系、31 個 background loop 的 identity 策略 關聯ADR-106架構、ADR-107儲存、ADR-123background loop migration


背景

AWOOOI 作為 AwoooP 的 first tenant 和 first runtime host在系統啟動時存在一個根本矛盾

  • AwoooP 的核心原則是「所有操作必須帶 project_id」
  • apps/api/src/main.py31 個 background looponboarder 實測),全部都沒有 project_id
  • AWOOOI 既是「需要被 project_id 保護的 tenant」又是「在任何 project 被建立前就要運行的平台基礎設施」

這就是 Bootstrap Paradox:你需要平台先啟動,才能建立 project但啟動時又沒有 project。


問題清單

  1. 31 個 background loop 無 project_idmain.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 migrationsawooop_projects + awooop_contract_revisions 等)
2. Seed AWOOOI project_idINSERT 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 2seed 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 實作

# 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 完成後)

# 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 contextPR-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-110GCP Ollama 拓撲)
  • ADR-123Background Loop Migration Strategy配套 ADR
  • INV-3Entrypoints、INV-8Background Loop Catalog