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:
4
apps/api/src/api/v1/platform/__init__.py
Normal file
4
apps/api/src/api/v1/platform/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""AwoooP Platform API — Phase 4 Shadow Mode Shell"""
|
||||
from src.api.v1.platform.runs import router
|
||||
|
||||
__all__ = ["router"]
|
||||
149
apps/api/src/api/v1/platform/runs.py
Normal file
149
apps/api/src/api/v1/platform/runs.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Platform Runs API
|
||||
==================
|
||||
AwoooP Phase 4: POST /v1/platform/runs — Shadow mode run 建立
|
||||
2026-05-04 ogt + Claude Sonnet 4.6(ADR-106/ADR-114)
|
||||
|
||||
禁止碰:
|
||||
- /v1/incidents/ — legacy 路由
|
||||
- /v1/webhooks/ — legacy 路由
|
||||
- Telegram bot handler — legacy 維持
|
||||
|
||||
Shadow mode 保證(Phase 4):
|
||||
- 建立的 run 全部 is_shadow=True
|
||||
- 不發送任何 user-visible response
|
||||
- 不執行任何 destructive tool call
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.services.audit_sink import write_audit
|
||||
from src.services.platform_runtime import create_run
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Request / Response models
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class CreateRunRequest(BaseModel):
|
||||
"""POST /v1/platform/runs request body"""
|
||||
|
||||
project_id: str = Field(..., description="租戶 ID")
|
||||
agent_id: str = Field(..., description="執行此 run 的 agent ID")
|
||||
trigger_type: str = Field(
|
||||
...,
|
||||
pattern="^(channel_event|schedule|api|sub_agent|retry)$",
|
||||
description="觸發來源類型",
|
||||
)
|
||||
trigger_ref: str | None = Field(None, description="觸發來源 ref(channel_event_id 等)")
|
||||
input_payload: dict[str, Any] | None = Field(None, description="Run 輸入 payload")
|
||||
channel_type: str | None = Field(None, description="Channel 類型(idempotency 用)")
|
||||
provider_event_id: str | None = Field(
|
||||
None, max_length=256,
|
||||
description="Channel provider 原始事件 ID(idempotency 去重用)",
|
||||
)
|
||||
timeout_seconds: int = Field(600, ge=30, le=3600, description="Run 超時秒數")
|
||||
|
||||
|
||||
class CreateRunResponse(BaseModel):
|
||||
"""POST /v1/platform/runs response"""
|
||||
|
||||
run_id: str
|
||||
is_duplicate: bool = Field(description="True = 冪等命中,返回既有 run_id")
|
||||
is_shadow: bool = Field(True, description="Phase 4 固定 True")
|
||||
message: str
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Routes
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"/runs",
|
||||
response_model=CreateRunResponse,
|
||||
status_code=status.HTTP_202_ACCEPTED,
|
||||
summary="建立 Platform Run(Shadow Mode)",
|
||||
description=(
|
||||
"AwoooP Phase 4 Shadow Mode:建立新 run,非同步執行。\n\n"
|
||||
"- `is_shadow=true`:不產生任何 user-visible response\n"
|
||||
"- `is_duplicate=true`:冪等命中,返回既有 run_id(不建立新 run)\n"
|
||||
"- provider_event_id + channel_type 構成冪等 key(24h 視窗)"
|
||||
),
|
||||
)
|
||||
async def create_platform_run(
|
||||
request: CreateRunRequest,
|
||||
) -> CreateRunResponse:
|
||||
"""建立 shadow run。"""
|
||||
try:
|
||||
run_id, is_duplicate = await create_run(
|
||||
project_id=request.project_id,
|
||||
agent_id=request.agent_id,
|
||||
trigger_type=request.trigger_type,
|
||||
trigger_ref=request.trigger_ref,
|
||||
input_payload=request.input_payload,
|
||||
channel_type=request.channel_type,
|
||||
provider_event_id=request.provider_event_id,
|
||||
timeout_seconds=request.timeout_seconds,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Run 建立失敗: {exc}",
|
||||
) from exc
|
||||
|
||||
# Audit log(非阻擋)
|
||||
await write_audit(
|
||||
project_id=request.project_id,
|
||||
action="run.created",
|
||||
resource_type="run",
|
||||
resource_id=str(run_id),
|
||||
details={
|
||||
"agent_id": request.agent_id,
|
||||
"trigger_type": request.trigger_type,
|
||||
"is_duplicate": is_duplicate,
|
||||
"is_shadow": True,
|
||||
},
|
||||
)
|
||||
|
||||
return CreateRunResponse(
|
||||
run_id=str(run_id),
|
||||
is_duplicate=is_duplicate,
|
||||
is_shadow=True,
|
||||
message="Run 已接受(shadow mode)" if not is_duplicate else "冪等命中,返回既有 run_id",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/runs/{run_id}",
|
||||
summary="查詢 Run 狀態",
|
||||
)
|
||||
async def get_run_status(
|
||||
run_id: str,
|
||||
project_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""查詢單一 run 的 FSM 狀態。"""
|
||||
from src.services.platform_runtime import get_run_status as _svc_get_run_status
|
||||
|
||||
try:
|
||||
uid = uuid.UUID(run_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"run_id 格式錯誤: {exc}",
|
||||
) from exc
|
||||
|
||||
result = await _svc_get_run_status(uid, project_id)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"run {run_id!r} 不存在",
|
||||
)
|
||||
return result
|
||||
Reference in New Issue
Block a user