## 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>
10 KiB
10 KiB
ADR-UI-04: Approval Queue UI
狀態:Accepted 日期:2026-05-03(台北) 決策者:統帥 範圍:M7 Approval Queue + M8 Approval Decision 的詳細設計 關聯:ADR-UI-01(架構)、ADR-114(WAITING_APPROVAL state)、ADR-116(approval_token)
M7 — Approval Queue
頁面設計
路由:/awooop/approvals
即時更新(SSE)— 重要:approval 過期倒數
┌────────────────────────────────────────────────────────────────────────────┐
│ Approval Queue 🔵 3 pending │
├──────────────┬────────────────────────────┬────────────────┬──────────────┤
│ Run ID │ Request Summary │ Risk Level │ Expires in │
├──────────────┼────────────────────────────┼────────────────┼──────────────┤
│ abc123... │ k8s_exec_pod(api, restart) │ 🔴 HIGH │ 12:34 ⏱️ │
│ def456... │ k8s_scale_deployment(3→1) │ 🟡 MEDIUM │ 06:12 ⏱️ │
│ ghi789... │ pg_mutate(DELETE incidents) │ 🔴 HIGH │ 02:01 ⚠️ │
└──────────────┴────────────────────────────┴────────────────┴──────────────┘
倒數計時顏色:
-
10 分鐘:灰色
- 5-10 分鐘:⏱️ 橙色
- < 5 分鐘:⚠️ 紅色閃爍
- 過期:❌ 已過期(不可再 approve)
倒數計時邏輯(前端計算,不依賴 SSE 更新):
function TokenCountdown({ expiresAt }: { expiresAt: string }) {
const [remaining, setRemaining] = useState(() =>
Math.max(0, differenceInSeconds(new Date(expiresAt), new Date()))
)
useEffect(() => {
const interval = setInterval(() => {
setRemaining(prev => Math.max(0, prev - 1))
}, 1000)
return () => clearInterval(interval)
}, [])
const minutes = Math.floor(remaining / 60)
const seconds = remaining % 60
const isUrgent = remaining < 300 // < 5分鐘
return (
<span className={isUrgent ? "text-red-500 animate-pulse" : "text-orange-400"}>
{minutes}:{seconds.toString().padStart(2, "0")}
</span>
)
}
Telegram 通知連動:
- 新的 WAITING_APPROVAL run → 同時發 Telegram 通知給有
approverrole 的 principal - 通知格式(ADR-116 + approval_token):
[APPROVAL REQUEST] Run abc123 Operation: k8s_exec_pod(api, restart) Risk: HIGH | Expires: 15min [Approve] [Reject] ← Telegram inline buttons - Telegram approve/reject → 走 callback → Phase 8 resume API
M8 — Approval Decision
頁面設計
路由:/awooop/approvals/[run_id]
頁面佈局:
┌─────────────────────────────────────────────────────────────────────────┐
│ Approval Request Expires: 12:34 ⏱️ │
├─────────────────────────────────────────────────────────────────────────┤
│ Run: abc12345-... │
│ Agent: openclaw-sre | Project: awoooi │
│ Subject: awoooi:telegram:123456789 (陳統帥) │
│ Trigger: "請幫我把 api deployment 縮到 1 個 replica" │
├─────────────────────────────────────────────────────────────────────────┤
│ REQUESTED OPERATION │
│ ────────────────────────────────────────────────────────────────────── │
│ Tool: k8s_scale_deployment │
│ Risk Level: 🔴 HIGH │
│ Parameters: │
│ namespace: awoooi-prod │
│ deployment: api │
│ replicas: 1 (current: 3) │
│ │
│ Compensation available: ✅ (scale back to 3 if rejected/rolled back) │
├─────────────────────────────────────────────────────────────────────────┤
│ SAGA Context(前面做了什麼) │
│ ────────────────────────────────────────────────────────────────────── │
│ Step 1 ✅ LLM decision: scale api to 1 (reason: SLO budget exceeded) │
│ Step 2 ⏳ WAITING: scale_deployment approval │
├─────────────────────────────────────────────────────────────────────────┤
│ APPROVAL DECISION │
│ │
│ Comment (optional): ____________________________________ │
│ │
│ [✅ Approve] [❌ Reject] │
└─────────────────────────────────────────────────────────────────────────┘
Approve / Reject 流程
async function handleApprovalDecision(
runId: string,
decision: "approve" | "reject",
comment: string
) {
// 1. 取得 approval_token(Platform API 生成 HS256 token)
const { approval_token } = await fetchApprovalToken(runId)
// 2. 送出決策
const response = await fetch(`/v1/platform/runs/${runId}/resume`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
approval_token, // ADR-116 D4
decision,
comment,
approver_subject_id: currentUser.platform_subject_id
})
})
if (!response.ok) {
const error = await response.json()
if (error.code === "E-APPROVAL-002") {
// approval_token 過期 → 提示用戶刷新頁面
toast.error("Approval token has expired. Please refresh.")
} else if (error.code === "E-APPROVAL-003") {
// jti 已使用(重複提交)
toast.error("This approval has already been submitted.")
}
return
}
// 3. 成功 → 跳回 Approval Queue
router.push("/awooop/approvals")
toast.success(`Run ${decision === "approve" ? "approved" : "rejected"} successfully.`)
}
錯誤處理
| 錯誤碼 | UI 行為 |
|---|---|
| E-APPROVAL-001 | 「此 run 不再需要 approval」→ 跳回 Queue |
| E-APPROVAL-002 | 「Token 已過期,請刷新」→ 重新取 token |
| E-APPROVAL-003 | 「已送出過 approval,請勿重複提交」→ 禁用按鈕 |
| E-APPROVAL-004 | 「您沒有 approver 權限」→ 顯示錯誤,跳回首頁 |
Token 取得機制
// GET /v1/platform/runs/{run_id}/approval-token
// → 返回 HS256 approval_token(只對目前 session 的用戶有效)
async function fetchApprovalToken(runId: string): Promise<{ approval_token: string }> {
const response = await fetch(`/v1/platform/runs/${runId}/approval-token`)
if (!response.ok) throw new Error("Failed to fetch approval token")
return response.json()
}
注意:approval_token 在 API 端生成後,每次頁面載入都重新取得(15min TTL,不快取)。
後果
Benefits
- 倒數計時讓 approver 清楚知道緊迫程度(防止 approval 超時)
- SAGA Context 顯示:approver 可以看到 run 到目前為止做了什麼,做出知情決策
- Compensation 顯示:approver 知道 reject 後會自動回滾什麼操作
- Telegram 按鈕連動:approver 可以在 Telegram 直接 approve/reject,不需要開網頁
Costs
- 倒數計時 setInterval(每頁面一個,不影響效能)
- approval_token 每次頁面載入都重新取得(一次 API call,可接受)
Risks
- 倒數計時與 server 時鐘有偏差(前端計算 vs server exp claim)
- 緩解:頁面載入時以 server 返回的
expires_at為準,不用Date.now()
驗收標準
- M7:WAITING_APPROVAL run 即時顯示 + 倒數計時(Phase 5)
- M7:< 5 分鐘 → 紅色閃爍(視覺測試)
- M8:SAGA Context 正確顯示前序步驟(Phase 5)
- M8:Approve → run 恢復 RESUMED 狀態(整合測試)
- M8:Reject → SAGA 補償觸發(整合測試)
- M8:E-APPROVAL-002(token 過期)→ 錯誤提示正確(整合測試)
- Telegram inline button approve → Phase 8 resume API(整合測試)
關聯
- ADR-UI-01(Operator Console 路由架構)
- ADR-UI-03(M6 Run Detail,SAGA 步驟資料共用)
- ADR-114(WAITING_APPROVAL state,lease 不 renew)
- ADR-116(approval_token HS256,M8 核心)
- ADR-119(SAGA compensation,reject 觸發)