# Gateway Functional Contract

> 目標：把 Hermes gateway 在 **identity 綁定、長任務交付、LINE quota fallback、email 檔案交付、shared drive 下載工作流** 的可接受行為定義成明確契約，未來更新或重構時可直接逐條核對。

## 實際程式碼再確認

本文件依下列 live 模組與測試整理：

- `gateway/user_verification.py`
- `gateway/run.py`
- `gateway/platforms/base.py`
- `plugins/platforms/line/adapter.py`
- `plugins/platforms/email/adapter.py`
- `hermes_cli/managed_downloads.py`
- `hermes_cli/web_server.py`
- `tests/gateway/test_line_plugin.py`
- `tests/gateway/test_email.py`
- `tests/gateway/test_approval_prompt_redaction.py`
- `tests/gateway/test_display_config.py`
- `tests/hermes_cli/test_managed_downloads.py`
- `tests/hermes_cli/test_web_verification_admin.py`
- `tests/gateway/test_principal_profile_routing_phase1.py`
- `tests/gateway/test_verified_email_handoff_guard_helpers.py`

這份文件只記錄目前已在程式或測試中有明確落點的行為，不把尚未實作的理想狀態假裝成既有功能。

---

## 1. Principal / verified email 綁定契約

### 1.1 authoritative source
- verified email 的 live 來源是 `GatewayUserStore`。
- 主要查詢入口：`GatewayUserStore.get_verified_email_for_source(source)`。
- 系統不得只靠對話上下文、記憶摘要、舊 prompt、舊 session 欄位決定要寄到哪個 email。

### 1.2 handoff 前必做 live confirm
- 任何 **系統主動** 把結果寄到使用者綁定信箱的流程，在寄出前都必須重新確認：
  - active source 的綁定 email
  - 與即將寄送的 target email
  - 必須完全一致
- 目前這條 hard guard 已有明確函式：
  - `GatewayUserStore.bound_email_matches_source(...)`
  - LINE 側經 `LineAdapter._verified_email_target_if_bound(...)` 使用

### 1.3 例外規則
- 若是 **使用者明確指定其他 email**，可用 `explicit_user_requested=True` 略過 bound-email equality check。
- 這個例外只適用使用者明講的目的地，不適用系統自行 fallback 或自動推論。

### 1.4 不可接受行為
- 把別的 principal 的 verified email 當成目前 source 的綁定信箱
- 用 stale session state / context compression 結果直接寄信
- 無 live confirm 就直接把 LINE / Telegram / OpenWebUI 的結果轉寄到 email

### 1.5 principal context 與 profile routing
- verified source 除了 email handoff guard，還會產生 principal context：
  - `GatewayUserStore.get_principal_context(source)`
  - `gateway/principal_profiles.py`
- verified principal 若啟用 `principal_profile_routing`，應穩定解析到同一 principal profile。
- 不可因 update / restart 導致同一 principal 突然丟失 profile mapping，否則會讓該 principal 的後續權限、工具、長任務脈絡一起漂移。

---

## 2. LINE 長任務契約

### 2.1 pending / postback state machine
- LINE 慢回覆流程使用 `RequestCache` 狀態機：
  - `PENDING`
  - `READY`
  - `DELIVERED`
  - `ERROR`
- `show_response` postback 只在該 cache 路徑中取回結果。
- 若任務還沒完成，`show_response` 必須回 pending 狀態，不得假裝 final 已送達。

### 2.2 pending button 的語意
- pending button 是「稍後取回答案」機制，不是 final delivery 本身。
- 若 final 結果已轉到 email surface，後續 postback replay 必須保留「已改寄 email」事實，不能回 generic delivered copy。

### 2.3 長任務 heartbeat
- gateway 允許 long-running heartbeat，但 LINE 預設應是 **安靜、泛化、對客版**。
- `tests/gateway/test_display_config.py` 已明確固定：
  - `tool_progress == off`
  - `busy_ack_detail == false`
  - `long_running_notifications == generic`
- 不應把 raw tool names、iteration counter、工程英文直接丟給 LINE 使用者，除非明確 opt-in。

### 2.4 non_conversational 與 bypass
- system busy-acks / approval / update 類通知必須以 `non_conversational` metadata 保護，不可被誤收進最終交付內容。
- heartbeat 與 background/system notices 不得誤變成使用者的 final email。

### 2.5 不可接受行為
- heartbeat 冒充 final answer
- `查看答案` 重播時遺失 fallback surface 狀態
- 長任務提示持續噴工程實作細節，讓使用者以為系統異常

---

## 3. LINE quota fallback → email 契約

### 3.1 fallback 觸發條件
- LINE push quota / push send 失敗時，應嘗試改走 verified email fallback。
- fallback payload 必須明確標記為：
  - `email_fallback_pending`
  - 或 `email_fallback`

### 3.2 pending 與 final 要分開
- pending 階段：
  - 可寄一次「仍在處理中，完成後會寄到驗證 email」通知
  - 應避免重複寄一堆同樣 pending 通知
- final 階段：
  - 應寄出完整內容
  - 並明確告知已改由 email 交付

### 3.3 fallback ownership transfer
- 一旦 final answer 已由 email fallback 接手，後續 post-stream 附件交付也必須沿用同一 email surface。
- 不得又回頭走 LINE 原生檔案送出，造成：
  - 使用者已收到 email，卻又看到 LINE 附件送失敗
  - 或 delivery state 自相矛盾

### 3.4 replay contract
- `DELIVERED` 狀態若 payload 為 `email_fallback`，replay / postback 必須重播 fallback notice，而不是 generic `Already replied` 類文字。

### 3.5 不可接受行為
- LINE quota 滿了卻靜默失敗
- fallback email 只寄文字，不帶原本該帶的檔案資訊或下載連結
- 使用者之後查詢結果時，看不到「其實已改寄 email」這個事實

---

## 4. Email 檔案交付契約

### 4.1 MIME attachment contract
- 若有附件且未超限：
  - email 應為 multipart
  - 要附真正 MIME attachment
  - 檔名要保留副檔名
  - `Content-Disposition` / `Content-Type` filename 參數要正確
- `tests/gateway/test_email.py` 已固定這個契約

### 4.2 HTML + plain text contract
- email 不只 plain text，也要帶 HTML body
- 裸 URL 在 HTML 中應轉成可點擊的 `<a href=...>`

### 4.3 large attachment contract
- 若附件總大小超過 email 限制：
  - 不應狂寄多封
  - 應改成單封通知 + 下載連結
  - payload 應保留：
    - `attachments_omitted_reason = size_limit`
    - `omitted_filenames`
    - `download_links`

### 4.4 download link contract
- 對客下載連結應優先使用 managed downloads 正式網域
- 目前由：
  - `hermes_cli/managed_downloads.py`
  - `hermes_cli/web_server.py` 的 `/downloads/{token}`
- token 必須可 resolve，且 route 必須真的能回檔案
- `tests/hermes_cli/test_managed_downloads.py` 已固定：
  - URL shape
  - route 可下載
  - `Content-Disposition` 帶原始副檔名

### 4.5 不可接受行為
- 說有附件，實際沒有 MIME attachment
- 附件有帶出但沒有副檔名
- 下載連結指到錯誤網域或重啟後失效 token
- 附件太大寄失敗後，沒有自動切成 link-only

---

## 5. Approval / 對客通知契約

### 5.1 approval 文案
- approval fallback 文案應為對客繁中，不應把 raw command / security scan 原文直接外露。
- 需要保留「可批准 / 拒絕」操作語意，但不洩漏敏感內容。

### 5.2 approval redaction seam
- `_redact_approval_command(...)` 是 approval prompt 的硬性 secret-egress boundary。
- chat platform path 與 SSE/API path 都必須在送出前實際使用 redacted command。
- `tests/gateway/test_approval_prompt_redaction.py` 已釘住這條契約。

---

## 6. Shared drive workflow 契約

### 6.1 查找與選檔契約
- shared drive 對話式工作流目前落在：
  - `~/.hermes/scripts/shared_drive_index_query.py`
  - `~/.hermes/scripts/shared_drive_lookup.py`
  - `~/.hermes/scripts/shared_drive_chat_workflow.py`
- `search` 必須回傳可對話使用的候選清單，且每個候選都帶 `file_id`。
- 若命中資料夾，`children <folder_file_id>` 必須能展開子檔案清單，供使用者最多選 10 檔。

### 6.2 打包與敏感檔契約
- `bundle --file-ids ... --requester <email>` 必須：
  - 以 `file_id` 解析實際檔案
  - 先複製到本機 staging
  - 產生 24h managed download link
- 若命中個資 / 財務或無法可靠判讀內容，必須自動改走加密 zip，並保留第二通道密碼通知資訊。

### 6.3 驗證契約
- bundle 結果必須帶 `verification` 欄位。
- 成功判準以 **真實 GET** 為準，不以 HEAD 為準。
- 不可把 `urllib` / Cloudflare `1010` 這類 bot-like 驗證誤判直接當成下載路徑壞掉。

### 6.4 不可接受行為
- search 找得到候選，但沒有 `file_id`，導致無法進入選檔/打包流程
- children 展開不到資料夾內容，卻沒說明是 index 或 parent mapping 問題
- 敏感檔仍走未加密原檔
- 連結其實可用，卻因 HEAD/urllib 驗證誤判為壞掉

---

## 7. 額度與權限治理契約

### 6.1 governance store 是 live source of truth
- principal、external identity、quota policy、model policy、dashboard role、admin audit log 的 authoritative store 都在 `GatewayUserStore` / `gateway/user_verification.py`。
- schema 至少包含：
  - `principals`
  - `external_identities`
  - `identity_enforcement`
  - `quota_policies`
  - `model_policies`
  - `dashboard_roles`
  - `principal_usage_daily`
  - `admin_audit_logs`

### 6.2 dashboard role contract
- dashboard verification/admin 角色目前分三級：
  - `viewer`
  - `operator`
  - `admin`
- 權限邏輯由 `hermes_cli/web_server.py::_require_verification_role(...)` 控制。
- 在 gated auth 模式下：
  - `viewer`：可讀 identities / principals / audit / quota / model / usage
  - `operator`：可 force-bind、unbind、block、unblock、resend verification、寫 quota/model policy
  - `admin`：可管理 dashboard role 本身
- 在 loopback / 非 auth_required 模式下，server 目前視為 `admin`；這是便利行為，不是未來所有部署都該仰賴的安全模型。

### 6.3 quota policy contract
- quota policy 目前已落地的資料欄位是：
  - `daily_message_limit`
  - `daily_token_limit`
  - `notes`
- principal usage 目前已落地的統計欄位是：
  - `message_count`
  - `input_tokens`
  - `output_tokens`
  - `total_tokens`
  - `last_model`
- `gateway/run.py` 在對話完成後會呼叫 `GatewayUserStore.record_usage_for_source(...)` 寫入 usage。

### 6.4 quota 的目前邊界
- 目前 repo 內已明確存在的是：
  - quota policy CRUD
  - usage record / usage list
  - API / admin 測試 round-trip
- 目前這一版文件 **不把尚未在 live code 看到的硬性 runtime deny / throttle** 假裝成已完整存在。
- 換句話說：目前 governance 比較像「可觀測 + 可設定 + 可對帳」，不是已全面接管所有 gateway runtime 拒絕邏輯。

### 6.5 model policy contract
- 每個 principal 可有：
  - `allowed_models`
  - `default_model`
- 這些資料目前已可經 admin API round-trip 保存與查詢。
- 若未來要主張「特定 principal 只能用指定模型」，必須再補 runtime enforcement 驗證；沒有測到前，不可把它寫成既成事實。

### 6.6 admin audit contract
- 任何人工治理動作至少應留下 audit：
  - force bind
  - unbind
  - block / unblock
  - role assignment
  - quota / model policy 變更（若後續擴充，應一併補 audit）
- 目前已確認 `force_bind_identity` 等身份治理動作可經 `/api/admin/verification/audit` 查回。

### 6.7 不可接受行為
- principal 還在，但 quota / model policy / role 因 update 被洗掉或查不到
- dashboard 可看 identities，卻看不到 usage / quota 對帳
- 系統把「有 quota policy 紀錄」誤當成「runtime 一定已 enforce」

---

## 7. 目前管理平台功能契約（current admin surface）

### 7.1 已確認存在的管理後台能力
依 `hermes_cli/web_server.py` 與 `tests/hermes_cli/test_web_verification_admin.py`，目前至少已有這些 admin API：

- `GET /api/admin/verification/role`
- `GET/POST /api/admin/verification/roles`
- `GET /api/admin/verification/identities`
- `GET /api/admin/verification/principals`
- `POST /api/admin/verification/force-bind`
- `POST /api/admin/verification/unbind`
- `POST /api/admin/verification/block`
- `POST /api/admin/verification/unblock`
- `POST /api/admin/verification/resend`
- `GET /api/admin/verification/audit`
- `GET/POST /api/admin/quota-policies`
- `GET/POST /api/admin/model-policies`
- `GET /api/admin/principal-usage`

### 7.2 這代表「目前管理平台」最少能做什麼
- 查誰綁了哪個平台 identity
- 查哪些 identities 被綁到同一 principal
- 人工 force-bind / unbind / block / unblock
- resend verification email
- 指派 dashboard role
- 設 quota / model policy
- 查每日 principal usage
- 查 admin audit log

### 7.3 目前不要過度聲稱的部分
- 我這次沒在 live code 裡看到一個已完成、明確對應上述所有治理 API 的前端 SPA 頁面證據。
- 所以目前比較穩的說法是：
  - **治理後台後端能力已存在並有測試覆蓋**
  - **前端顯示/操作面是否完整暴露全部能力，仍需額外做 UI 對照驗證**
- 這可以避免未來有人誤以為「API 有 = UI 一定完整可用」。

---

## 8. 這份契約如何維護

每次修改以下任一區塊，都要同步檢查本文件是否要更新：

- `gateway/user_verification.py`
- `gateway/run.py`
- `gateway/platforms/base.py`
- `plugins/platforms/line/adapter.py`
- `plugins/platforms/email/adapter.py`
- `hermes_cli/managed_downloads.py`
- `hermes_cli/web_server.py`
- 對應 tests

若改動造成契約變更，應同時更新：
1. 本文件
2. `gateway-state-and-sequence.md`
3. `gateway-regression-matrix.md`
4. 對應 regression tests
