# ADR-115: Canonical Principal Mapping & Tenant Onboarding Patterns

**狀態**：Accepted
**日期**：2026-05-03（台北）
**決策者**：統帥
**範圍**：所有 channel 的使用者身份統一映射、EwoooC Provider Proxy Adapter 設計
**關聯**：ADR-106（Communication contract）、ADR-107（儲存）、ADR-110（GCP Ollama）

---

## 背景

問題 1 — **Principal 分裂**：
- Telegram 使用者有 `user_id` + `chat_id`
- LINE 使用者有 `userId` + `chatId`（格式不同）
- API 呼叫者有 `api_key` 或 `jwt_sub`
- 內部 service 呼叫有 service account name
- 沒有統一的身份映射，多 tenant 場景下無法做 ACL 和 audit

問題 2 — **EwoooC 接入零技術路徑**：
- MASTER-WORKPLAN P1-02：不能只改 `OLLAMA_API_BASE`
- EwoooC 需要完整的 `project_id` / `agent_id` / `trace_id` / `run_id` envelope
- 否則 budget、audit、policy 全部無效

問題 3 — **telemetry.py:71 硬碼 IP**（P0-08）：
- `if "192.168.0.188" not in endpoint: raise` 阻止 EwoooC 使用不同的 OTEL endpoint
- 已列入 INV-4，PR-01 修補

---

## 決策

### D1 — Canonical Platform Subject（統一身份）

所有 channel 的使用者映射到 `awooop_platform_subjects` 表：

```sql
CREATE TABLE awooop_platform_subjects (
    subject_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id      VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
    -- 原始身份（channel-specific）
    channel_type    VARCHAR(32) NOT NULL,   -- telegram/line/slack/api/internal
    channel_user_id VARCHAR(256) NOT NULL,  -- Telegram: user_id, LINE: userId
    channel_chat_id VARCHAR(256),           -- Telegram: chat_id, LINE: chatId
    -- 統一身份
    platform_subject_id VARCHAR(128) NOT NULL,  -- {project_id}:{channel_type}:{channel_user_id}
    display_name    VARCHAR(256),
    -- Roles / ACL
    roles           JSONB NOT NULL DEFAULT '[]',  -- ["admin", "approver", "viewer"]
    -- Metadata
    first_seen_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_seen_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(project_id, channel_type, channel_user_id)
);
```

**platform_subject_id 格式**：
```
{project_id}:{channel_type}:{channel_user_id}

例：
  awoooi:telegram:123456789
  ewoooc:line:Uf987654321
  awoooi:api:svc-account-k8s-monitor
  awoooi:internal:slo-watchdog-job
```

### D2 — Principal Mapping 規則

| Channel | channel_user_id 取自 | channel_chat_id 取自 |
|---------|---------------------|---------------------|
| Telegram | `message.from.id` | `message.chat.id` |
| LINE | `event.source.userId` | `event.source.groupId` / `roomId` |
| Slack | `event.user` | `event.channel` |
| API（JWT）| JWT `sub` claim | N/A |
| API（API key）| api_key SHA256 prefix | N/A |
| Internal service | service account name | N/A |

**Auto-provisioning 規則**：
- 新 channel user 第一次出現 → 自動 INSERT platform_subjects（roles=['viewer']）
- 管理員手動升級 roles（admin 操作）
- 敏感操作（approval、activate contract）：必須有 `approver` role

### D3 — EwoooC Provider Proxy Adapter

EwoooC 接入 AwoooP 的完整路徑（不只是改 URL）：

```
EwoooC 請求
   ↓
Provider Proxy Adapter（新建 apps/api/src/services/provider_proxy.py）
   ↓
注入 envelope：
  {
    "project_id": "ewoooc",
    "agent_id": "openclaw-biz",
    "trace_id": "<W3C traceparent>",
    "run_id": "<UUID>",
    "policy_revision_id": "<active policy revision>",
    "channel_type": "telegram",
    "platform_subject_id": "ewoooc:telegram:{user_id}"
  }
   ↓
EffectivePolicy 解析（ewoooc 的 policy contract）
   ↓
Budget Guard（ewoooc 的 budget_ledger）
   ↓
GCP Ollama 路由（platform:ollama:topology，三層共用）
   ↓
Audit（project_id=ewoooc）
```

**關鍵限制（EwoooC 接入時）**：
- EwoooC 使用 GCP Ollama 三層拓撲（ADR-110），不能指定自己的 Ollama host
- EwoooC budget 計數在 `budget_ledger` 的 `project_id=ewoooc` partition，與 AWOOOI 隔離
- EwoooC 的 run 不可讀取 AWOOOI 的 context、incident、KM（RLS 保護）

### D4 — 其他 Tenant Onboarding 模板（Tsenyang / Bitan）

每個新 tenant 的 onboarding 步驟：

```
1. INSERT INTO awooop_projects(project_id, display_name, migration_mode='shadow')
2. 建立 Project/Tenant contract（版本 v1.0，包含 allowed_k8s_namespaces、budget_limit、channel_config）
3. 建立 Agent contract（至少一個 agent）
4. 建立 Policy/Routing contract（model routing policy）
5. Provider Proxy Adapter 設定（指向 platform GCP Ollama topology）
6. 第一個 shadow run 驗證（確認 project_id 正確、audit 有資料）
7. 進入 shadow → canary gate 觀察期
```

### D5 — 跨 Tenant 隔離驗證

以下操作必須在 EwoooC onboarding 時通過 vuln-verifier PoC：
- `SELECT * FROM incidents WHERE incident_id = '<AWOOOI incident>'` 在 ewoooc project context → 空結果（RLS 拒絕）
- `GET /v1/platform/runs/{awoooi_run_id}` 帶 ewoooc token → 403 E-TENANT-001
- EwoooC LLM call 的 token 計數進 ewoooc budget，不進 awoooi budget

---

## 後果

### Benefits
- 所有 channel 的使用者身份統一，ACL 和 audit 可以跨 channel 查詢
- EwoooC 接入有完整 envelope，budget / audit / policy 全部有效
- Tsenyang / Bitan 未來接入有現成模板，不需要重新設計

### Costs
- 需要建立 `awooop_platform_subjects` 表並在每次請求時 upsert（IO 成本）
- Provider Proxy Adapter 是新建模組（約 300-500 行）

### Risks
- platform_subject_id 格式若未來 channel_user_id 變化 → 需要 migration
- 緩解：UUID `subject_id` 是 PK，`platform_subject_id` 只是顯示欄位，改 format 只需更新 display，不影響 FK
- EwoooC Provider Proxy 若 latency 過高 → EwoooC 回應慢
- 緩解：Proxy 只做 envelope 注入（<1ms），不做額外 IO

---

## 驗收標準

- [ ] `awooop_platform_subjects` 表建立（Phase 1 migration）
- [ ] Telegram 新 user 第一次請求 → 自動建立 platform_subject（整合測試）
- [ ] Provider Proxy Adapter 建立（Phase 6 before EwoooC onboarding）
- [ ] EwoooC run 無法讀取 AWOOOI data（cross-tenant RLS vuln-verifier PoC）
- [ ] EwoooC budget 與 AWOOOI budget 獨立計算（整合測試）
- [ ] `telemetry.py:71` hardcoded IP assert 已移除（PR-01 完成，Phase 2 前）

## 關聯

- ADR-106（Communication contract）、ADR-107（儲存）
- ADR-110（GCP Ollama 三層拓撲，EwoooC 共用）
- ADR-116（security hardening）、ADR-118（RLS）
- INV-4（hardcoded IP，telemetry.py:71 修補）
