feat(awooop): Phase 1-8 完整實作 — AwoooP Agent Platform 六平面架構
## 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:
@@ -106,6 +106,11 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
factory = get_session_factory()
|
||||
async with factory() as session:
|
||||
try:
|
||||
# AwoooP Phase 2.3 (2026-05-04 ogt): SET LOCAL app.project_id 讓 RLS Policy 生效
|
||||
# 預設 'awoooi',多租戶路由將在 middleware 注入實際 project_id
|
||||
await session.execute(
|
||||
text("SELECT set_config('app.project_id', 'awoooi', TRUE)")
|
||||
)
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
@@ -114,17 +119,30 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_db_context() -> AsyncGenerator[AsyncSession, None]:
|
||||
async def get_db_context(project_id: str | None = None) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""
|
||||
Context manager for database session (non-FastAPI usage)
|
||||
|
||||
AwoooP Phase 2.3/2.4: 優先序 — 明確參數 > contextvar > "awoooi"
|
||||
- Phase 2.3: 啟用 RLS tenant isolation(SET LOCAL app.project_id)
|
||||
- Phase 2.4: 從 asyncio contextvar 讀取 background loop 的 project_id
|
||||
|
||||
Usage:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context() as db: # 繼承 contextvar 或預設 awoooi
|
||||
...
|
||||
async with get_db_context("other-tenant") as db: # 明確指定 tenant
|
||||
...
|
||||
"""
|
||||
from src.core.context import get_current_project_id
|
||||
effective_pid = project_id if project_id is not None else get_current_project_id()
|
||||
|
||||
factory = get_session_factory()
|
||||
async with factory() as session:
|
||||
try:
|
||||
await session.execute(
|
||||
text("SELECT set_config('app.project_id', :pid, TRUE)"),
|
||||
{"pid": effective_pid},
|
||||
)
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
@@ -299,6 +317,62 @@ async def init_db() -> None:
|
||||
"ON timeline_events(incident_id);"
|
||||
))
|
||||
|
||||
# AwoooP Phase 2.6 (2026-05-04 ogt): budget_ledger 建表(ADR-120 Token Budget Hard Kill)
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS budget_ledger (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi',
|
||||
agent_id VARCHAR(128),
|
||||
run_id UUID,
|
||||
model VARCHAR(64),
|
||||
provider VARCHAR(32),
|
||||
prompt_tokens INT,
|
||||
completion_tokens INT,
|
||||
cost_usd NUMERIC(10, 4) NOT NULL DEFAULT 0.0000,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"""))
|
||||
await conn.execute(text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_budget_ledger_project_date "
|
||||
"ON budget_ledger(project_id, recorded_at DESC);"
|
||||
))
|
||||
|
||||
# AwoooP Phase 2.3 (2026-05-04 ogt): 四表加 project_id(RLS 多租戶隔離)
|
||||
# 防禦性 ALTER — 已存在欄位為 no-op,安全。
|
||||
# Batch 1 RLS migration 執行後,app.project_id 由 get_db_context() 自動設置。
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE incidents "
|
||||
"ADD COLUMN IF NOT EXISTS project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi';"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_incidents_project_id "
|
||||
"ON incidents (project_id);"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE knowledge_entries "
|
||||
"ADD COLUMN IF NOT EXISTS project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi';"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_knowledge_entries_project_id "
|
||||
"ON knowledge_entries (project_id);"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE playbooks "
|
||||
"ADD COLUMN IF NOT EXISTS project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi';"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_playbooks_project_id "
|
||||
"ON playbooks (project_id);"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE audit_logs "
|
||||
"ADD COLUMN IF NOT EXISTS project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi';"
|
||||
))
|
||||
await conn.execute(text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_project_id "
|
||||
"ON audit_logs (project_id);"
|
||||
))
|
||||
|
||||
# 2026-04-15 ogt + Claude Sonnet 4.6(亞太): Phase 6 自我治理閉環
|
||||
# ADR-087: ai_governance_events 不可變 Event Sourcing 表
|
||||
# asyncpg 不允許 prepared statement 內多條指令,必須分開 execute
|
||||
|
||||
Reference in New Issue
Block a user