Files
awoooi/apps/api/src/repositories/contract_repository.py
Your Name 8629ac709b
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
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>
2026-05-04 19:31:53 +08:00

262 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Contract Repository
===================
AwoooP Phase 3: contract revision CRUDappend-only
2026-05-04 ogt + Claude Sonnet 4.6ADR-107/ADR-112
設計原則:
- append-only已 published 的 revision 不可修改
- active pointer 以 UPSERT 維護awooop_active_revisions
- outbox 事件在同一 transaction 寫入ADR-113
- RLS 透過 get_db_context() 自動套用
"""
from __future__ import annotations
from typing import Any
from uuid import UUID
import structlog
from sqlalchemy import select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from src.db.awooop_models import (
AwoooPActiveRevision,
AwoooPContractOutbox,
AwoooPContractRevision,
)
from src.db.base import get_db_context
logger = structlog.get_logger(__name__)
# ─────────────────────────────────────────────────────────────────────────────
# Read
# ─────────────────────────────────────────────────────────────────────────────
async def get_revision(
revision_id: UUID,
project_id: str = "awoooi",
) -> AwoooPContractRevision | None:
"""依 revision_id 讀取單筆(含 RLS 驗證)"""
async with get_db_context(project_id) as db:
result = await db.execute(
select(AwoooPContractRevision).where(
AwoooPContractRevision.revision_id == revision_id,
AwoooPContractRevision.project_id == project_id,
)
)
return result.scalar_one_or_none()
async def get_active_revision(
project_id: str,
contract_family: str,
contract_id: str,
) -> AwoooPContractRevision | None:
"""
讀取 active revisionruntime 路徑)。
只返回 lifecycle_status='active' 的 revision。
"""
async with get_db_context(project_id) as db:
result = await db.execute(
select(AwoooPContractRevision)
.join(
AwoooPActiveRevision,
AwoooPActiveRevision.active_revision_id == AwoooPContractRevision.revision_id,
)
.where(
AwoooPActiveRevision.project_id == project_id,
AwoooPActiveRevision.contract_family == contract_family,
AwoooPActiveRevision.contract_id == contract_id,
AwoooPContractRevision.lifecycle_status == "active",
)
)
return result.scalar_one_or_none()
async def list_revisions(
project_id: str,
contract_family: str,
contract_id: str,
lifecycle_status: str | None = None,
) -> list[AwoooPContractRevision]:
"""列出所有 revision按 version 降序)"""
async with get_db_context(project_id) as db:
q = select(AwoooPContractRevision).where(
AwoooPContractRevision.project_id == project_id,
AwoooPContractRevision.contract_family == contract_family,
AwoooPContractRevision.contract_id == contract_id,
)
if lifecycle_status:
q = q.where(AwoooPContractRevision.lifecycle_status == lifecycle_status)
q = q.order_by(
AwoooPContractRevision.version_major.desc(),
AwoooPContractRevision.version_minor.desc(),
)
result = await db.execute(q)
return list(result.scalars().all())
# ─────────────────────────────────────────────────────────────────────────────
# Writeappend-only
# ─────────────────────────────────────────────────────────────────────────────
async def create_draft(
*,
project_id: str,
contract_family: str,
contract_id: str,
version_major: int,
version_minor: int,
body_json: dict[str, Any],
body_hash: str,
body_schema_version: str = "v1.0",
) -> AwoooPContractRevision:
"""建立 draft revision不可被 runtime 讀取)"""
async with get_db_context(project_id) as db:
revision = AwoooPContractRevision(
project_id=project_id,
contract_family=contract_family,
contract_id=contract_id,
version_major=version_major,
version_minor=version_minor,
lifecycle_status="draft",
body_json=body_json,
body_hash=body_hash,
body_schema_version=body_schema_version,
)
db.add(revision)
await db.flush()
await db.refresh(revision)
logger.info(
"contract_draft_created",
revision_id=str(revision.revision_id),
project_id=project_id,
contract_family=contract_family,
contract_id=contract_id,
)
return revision
async def mark_published(
*,
revision_id: UUID,
project_id: str,
publisher_id: str,
publish_signature: str,
published_at: Any, # datetime
) -> AwoooPContractRevision:
"""
draft → published 轉換HMAC 簽章驗證後由 service 呼叫)。
published revision 可被 activate但不可被 runtime 直接讀取。
"""
async with get_db_context(project_id) as db:
await db.execute(
update(AwoooPContractRevision)
.where(
AwoooPContractRevision.revision_id == revision_id,
AwoooPContractRevision.project_id == project_id,
AwoooPContractRevision.lifecycle_status == "draft",
)
.values(
lifecycle_status="published",
publisher_id=publisher_id,
publish_signature=publish_signature,
published_at=published_at,
)
)
result = await db.execute(
select(AwoooPContractRevision).where(
AwoooPContractRevision.revision_id == revision_id
)
)
revision = result.scalar_one()
logger.info(
"contract_published",
revision_id=str(revision_id),
project_id=project_id,
publisher_id=publisher_id,
)
return revision
async def mark_active(
*,
revision_id: UUID,
project_id: str,
contract_family: str,
contract_id: str,
old_revision_id: UUID | None,
) -> AwoooPContractRevision:
"""
published → active 轉換 + 更新 active pointer + 寫入 outbox。
三個操作在同一 transactionADR-113 transactional outbox
"""
async with get_db_context(project_id) as db:
# 1. 更新 revision lifecycle_status
await db.execute(
update(AwoooPContractRevision)
.where(
AwoooPContractRevision.revision_id == revision_id,
AwoooPContractRevision.project_id == project_id,
AwoooPContractRevision.lifecycle_status == "published",
)
.values(lifecycle_status="active")
)
# 2. UPSERT active pointer
stmt = pg_insert(AwoooPActiveRevision).values(
project_id=project_id,
contract_family=contract_family,
contract_id=contract_id,
active_revision_id=revision_id,
)
stmt = stmt.on_conflict_do_update(
constraint="uq_active_pointer",
set_={
"active_revision_id": revision_id,
},
)
await db.execute(stmt)
# 3. 寫入 outbox eventADR-113
outbox_event = AwoooPContractOutbox(
event_type="contract.activated",
project_id=project_id,
contract_family=contract_family,
contract_id=contract_id,
old_revision_id=old_revision_id,
new_revision_id=revision_id,
)
db.add(outbox_event)
# 4. 如有舊 active revision標記為 revoked
if old_revision_id:
await db.execute(
update(AwoooPContractRevision)
.where(
AwoooPContractRevision.revision_id == old_revision_id,
AwoooPContractRevision.lifecycle_status == "active",
)
.values(lifecycle_status="revoked")
)
result = await db.execute(
select(AwoooPContractRevision).where(
AwoooPContractRevision.revision_id == revision_id
)
)
revision = result.scalar_one()
logger.info(
"contract_activated",
revision_id=str(revision_id),
old_revision_id=str(old_revision_id) if old_revision_id else None,
project_id=project_id,
contract_family=contract_family,
contract_id=contract_id,
)
return revision