feat(awooop): Phase 1-8 完整實作 — AwoooP Agent Platform 六平面架構
Some checks failed
run-migration / migrate (push) Failing after 59s
Code Review / ai-code-review (push) Successful in 1m8s
Type Sync Check / check-type-sync (push) Successful in 2m27s

## Phase 1-3: Control Plane + Contract System
- awooop_phase1_control_plane_2026-05-04.sql: 12 張核心表 + RLS
- awooop_phase1_batch1_rls_2026-05-04.sql: 全部 FORCE RLS + GRANT
- packages/awooop-contracts/: 六合約 JSON Schema + golden fixtures
- src/models/awooop_contracts.py: Pydantic v2 contract models(extra=forbid)
- src/repositories/contract_repository.py: contract lifecycle(draft→published→active)
- src/services/contract_service.py: HMAC publish sig + Redis multi-sig activate
- src/services/schema_validator.py: LLM output validator(retry×3, E-SCHEMA-001)

## Phase 2: Tenant Isolation
- awooop_phase2_budget_ledger_2026-05-04.sql: budget_ledger + RLS
- src/services/budget_service.py: Token Budget Hard Kill 三層防線
- src/core/context.py: PROJECT_ID ContextVar(31 background loop 自動繼承)
- src/db/base.py + models.py: project_id 欄位 + RLS set_config 注入
- src/hermes/nl_gateway.py: project_id Redis key 前綴(Phase A 雙寫)
- src/services/anomaly_counter.py: per-project 改造(Phase A fallback)

## Phase 4: Platform Shell in Shadow Mode
- awooop_phase4_run_state_2026-05-04.sql: run_state + step_journal + idempotency
- src/services/run_state_machine.py: 8-state FSM + SKIP LOCKED + stale reaper
- src/services/platform_runtime.py: UUID v7 + W3C trace_id + shadow_execute
- src/services/audit_sink.py: PII/secret redaction 9 patterns
- src/api/v1/platform/runs.py: POST/GET /v1/platform/runs(Router→Service 架構)
- src/workers/platform_worker.py: SKIP LOCKED worker + heartbeat + reaper loop
- src/main.py: platform router + lifespan worker start/stop

## Phase 5: MCP Gateway 五閘門
- awooop_phase5_mcp_gateway_2026-05-04.sql: 4 表 + RLS
- src/plugins/mcp/gateway.py: McpGateway(Gate 1~5, E-MCP-GATE-001~009)
- src/plugins/mcp/redaction_middleware.py: 雙層 redaction + 16K 截斷
- src/plugins/mcp/registry.py: __provider name mangling(ADR-116)
- src/plugins/mcp/credential_resolver.py: k8s secret ref 解析
- tests/test_mcp_credential_isolation.py: 10 個迴歸測試(secret leak 防再現)

## Phase 6-8: EwoooC + Channel Hub + Approval Token
- awooop_phase6_ewoooc_onboarding_2026-05-04.sql: ewoooc tenant + 4 read-only MCP tools
- awooop_phase7_channel_hub_2026-05-04.sql: conversation_event + outbound_message
- src/services/provider_proxy.py: ProviderProxy + PlatformEnvelope(ADR-115)
- src/services/channel_hub.py: Telegram inbound mirror + Progressive Feedback(30s)
- src/services/awooop_approval_token.py: HS256 + jti NX replay 防護 + suggest mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-04 19:31:53 +08:00
parent 0a90dab1e9
commit 8629ac709b
69 changed files with 10999 additions and 77 deletions

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Telegram dedup 修復驗證 — commit b3a0f0d7 (fingerprint dedup + 24h TTL)
# 部署時間: 2026-05-02 16:25 Asia/Taipei
# 用法: ssh wooo@192.168.0.121 'bash -s' < verify_telegram_dedup_b3a0f0d7.sh
# 或 scp 上去後 sudo bash verify_telegram_dedup_b3a0f0d7.sh
# 純讀,不寫任何 prod 資料
set -e
POD=$(sudo kubectl get pods -n awoooi-prod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')
echo "=== Pod: $POD ==="
echo "=== Image SHA (應含 b3a0f0d7) ==="
sudo kubectl get pod -n awoooi-prod "$POD" -o jsonpath='{.spec.containers[0].image}'
echo
echo
echo "=== A. 過去 1h Telegram 發送 top部署後==="
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
import asyncio, os, asyncpg
async def q():
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
rows = await conn.fetch(\"\"\"
SELECT
COALESCE(i.title, 'unknown') AS alertname,
COALESCE(i.affected_services[1], 'unknown') AS target,
COUNT(t.id) AS msg_count,
MIN(t.created_at) AS first_sent,
MAX(t.created_at) AS last_sent
FROM notification_outcomes t
JOIN approval_records a ON t.approval_id = a.id
JOIN incidents i ON a.incident_id = i.id
WHERE t.channel='telegram' AND t.created_at > now() - interval '1 hour'
GROUP BY 1,2 ORDER BY 3 DESC LIMIT 10
\"\"\")
for r in rows:
print(f\" {r['msg_count']:>3} | {r['alertname'][:40]:<40} | {r['target'][:30]:<30} | first={r['first_sent']:%H:%M} last={r['last_sent']:%H:%M}\")
await conn.close()
asyncio.run(q())
"
echo
echo "=== B. 過去 24h含部署前對照==="
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
import asyncio, os, asyncpg
async def q():
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
rows = await conn.fetch(\"\"\"
SELECT
COALESCE(i.title, 'unknown') AS alertname,
COALESCE(i.affected_services[1], 'unknown') AS target,
COUNT(t.id) AS msg_count
FROM notification_outcomes t
JOIN approval_records a ON t.approval_id = a.id
JOIN incidents i ON a.incident_id = i.id
WHERE t.channel='telegram' AND t.created_at > now() - interval '24 hours'
GROUP BY 1,2 ORDER BY 3 DESC LIMIT 10
\"\"\")
for r in rows:
print(f\" {r['msg_count']:>3} | {r['alertname'][:40]:<40} | {r['target'][:30]:<30}\")
await conn.close()
asyncio.run(q())
"
echo
echo "=== C. 截圖兩 INC 最後發送時刻 ==="
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
import asyncio, os, asyncpg
async def q():
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
rows = await conn.fetch(\"\"\"
SELECT i.id, i.title, COUNT(t.id) AS total_24h,
MAX(t.created_at) AS last_sent,
COUNT(t.id) FILTER (WHERE t.created_at > '2026-05-02 16:25 Asia/Taipei'::timestamptz) AS post_deploy
FROM notification_outcomes t
JOIN approval_records a ON t.approval_id = a.id
JOIN incidents i ON a.incident_id = i.id
WHERE i.id IN ('INC-20260501-6FE3BD','INC-20260502-FD6E21')
AND t.channel='telegram' AND t.created_at > now() - interval '24 hours'
GROUP BY 1,2 ORDER BY 1
\"\"\")
for r in rows:
print(f\" {r['id']} | {r['title'][:40]:<40} | 24h={r['total_24h']} 部署後={r['post_deploy']} last={r['last_sent']:%H:%M}\")
await conn.close()
asyncio.run(q())
"
echo
echo "=== D. Redis dedup key 結構fingerprint 應已建立)==="
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
import asyncio, os
from redis.asyncio import Redis
async def q():
r = Redis.from_url(os.environ['REDIS_URL'])
fp_keys = await r.keys('telegram_sent:fp:*')
inc_keys = await r.keys('telegram_sent:INC-*')
print(f' telegram_sent:fp:* (新格式) = {len(fp_keys)} (應 > 0)')
print(f' telegram_sent:INC-* (舊格式) = {len(inc_keys)} (應 = 0 或減少中)')
if fp_keys:
print(f' 範例 fp key: {fp_keys[0].decode() if isinstance(fp_keys[0], bytes) else fp_keys[0]}')
sweeper_keys = await r.keys('sweeper_done:*')
print(f' sweeper_done:* = {len(sweeper_keys)} (24h TTL整個 INVESTIGATING 集合)')
asyncio.run(q())
"
echo
echo "=== 驗收標準 ==="
echo "✅ A 段任何 fingerprint msg_count ≤ 2 → 修復生效"
echo "✅ C 段兩 INC 部署後 ≤ 1 → 鐵證生效"
echo "✅ D 段 telegram_sent:fp:* 已建立 → 新 dedup 邏輯有跑"
echo "❌ 任何 fingerprint 部署後仍 ≥ 5 → 未生效,回報 Claude"