706 lines
30 KiB
Python
706 lines
30 KiB
Python
"""
|
||
AwoooP Control Plane Models
|
||
============================
|
||
Phase 1 新表:六合約 control plane、tenant 隔離、principal mapping。
|
||
ADR-111~118,2026-05-04 ogt + Claude Sonnet 4.6
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
from uuid import UUID
|
||
|
||
from sqlalchemy import (
|
||
Boolean,
|
||
CheckConstraint,
|
||
ForeignKey,
|
||
Index,
|
||
Integer,
|
||
Numeric,
|
||
SmallInteger,
|
||
String,
|
||
Text,
|
||
UniqueConstraint,
|
||
text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from src.db.base import Base
|
||
|
||
|
||
class AwoooPProject(Base):
|
||
"""租戶主表(ADR-111 bootstrap,ADR-115 tenant onboarding)"""
|
||
|
||
__tablename__ = "awooop_projects"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"migration_mode IN ('legacy_awoooi_default','shadow','canary','active')",
|
||
name="chk_migration_mode",
|
||
),
|
||
CheckConstraint(
|
||
"budget_limit_usd IS NULL OR budget_limit_usd >= 0",
|
||
name="chk_budget_non_negative",
|
||
),
|
||
CheckConstraint(
|
||
"jsonb_typeof(allowed_channels) = 'array'",
|
||
name="chk_allowed_channels_array",
|
||
),
|
||
)
|
||
|
||
project_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
display_name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
migration_mode: Mapped[str] = mapped_column(
|
||
String(32), nullable=False, default="legacy_awoooi_default"
|
||
)
|
||
budget_limit_usd: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 4), nullable=True
|
||
)
|
||
allowed_channels: Mapped[list[Any]] = mapped_column(
|
||
JSONB, nullable=False, server_default=text("'[]'::jsonb")
|
||
)
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPContractRevision(Base):
|
||
"""六合約共用 revision 表(append-only,ADR-107/ADR-112)"""
|
||
|
||
__tablename__ = "awooop_contract_revisions"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"project_id", "contract_family", "contract_id",
|
||
"version_major", "version_minor",
|
||
name="uq_revision_version",
|
||
),
|
||
CheckConstraint(
|
||
"contract_family IN ("
|
||
"'project_tenant','agent','mcp_gateway','policy_routing',"
|
||
"'runtime_run_state','channel_event','platform_resource')",
|
||
name="chk_contract_family",
|
||
),
|
||
CheckConstraint(
|
||
"lifecycle_status IN ('draft','published','active','revoked')",
|
||
name="chk_lifecycle",
|
||
),
|
||
CheckConstraint("version_major >= 0", name="chk_version_major_non_neg"),
|
||
CheckConstraint("version_minor >= 0", name="chk_version_minor_non_neg"),
|
||
CheckConstraint(
|
||
r"body_hash ~ '^[0-9a-f]{64}$'", name="chk_body_hash_format"
|
||
),
|
||
Index(
|
||
"idx_revisions_lookup",
|
||
"project_id", "contract_family", "contract_id",
|
||
"lifecycle_status", "version_major", "version_minor",
|
||
),
|
||
Index("idx_revisions_hash", "body_hash"),
|
||
)
|
||
|
||
revision_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
version_major: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1)
|
||
version_minor: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
lifecycle_status: Mapped[str] = mapped_column(
|
||
String(16), nullable=False, default="draft"
|
||
)
|
||
body_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
body_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
body_schema_version: Mapped[str] = mapped_column(
|
||
String(16), nullable=False, default="v1.0"
|
||
)
|
||
publish_signature: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
publisher_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
published_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPActiveRevision(Base):
|
||
"""Active revision pointer(ADR-107/ADR-113)"""
|
||
|
||
__tablename__ = "awooop_active_revisions"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"project_id", "contract_family", "contract_id",
|
||
name="uq_active_pointer",
|
||
),
|
||
)
|
||
|
||
pointer_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
active_revision_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_contract_revisions.revision_id", ondelete="RESTRICT"),
|
||
nullable=False,
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPContractOutbox(Base):
|
||
"""Transactional outbox for contract revision invalidation(ADR-113)"""
|
||
|
||
__tablename__ = "awooop_contract_outbox"
|
||
__table_args__ = (
|
||
UniqueConstraint("new_revision_id", "event_type", name="uq_outbox_event"),
|
||
Index(
|
||
"idx_outbox_pending",
|
||
"next_retry_at", "created_at",
|
||
postgresql_where=text("delivered_at IS NULL"),
|
||
),
|
||
Index(
|
||
"idx_outbox_backlog_per_project",
|
||
"project_id", "created_at",
|
||
postgresql_where=text("delivered_at IS NULL"),
|
||
),
|
||
)
|
||
|
||
event_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
old_revision_id: Mapped[UUID | None] = mapped_column(
|
||
ForeignKey("awooop_contract_revisions.revision_id"), nullable=True
|
||
)
|
||
new_revision_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_contract_revisions.revision_id"), nullable=False
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
delivered_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
relay_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
next_retry_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
||
|
||
class AwoooPChannelEventDedupe(Base):
|
||
"""Channel event idempotency key(ADR-114,partitioned by created_at)"""
|
||
|
||
__tablename__ = "awooop_channel_event_dedupe"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"project_id", "channel_type", "provider_event_id", "created_at",
|
||
name="uq_channel_event_dedupe",
|
||
),
|
||
Index("idx_dedupe_run", "run_id"),
|
||
)
|
||
|
||
# Composite PK(partition key 必須是 PK 一部分)
|
||
# SQLAlchemy 2.x 要求 primary_key=True 標在 mapped_column,不能用 __mapper_args__ 字串 list
|
||
dedupe_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
run_id: Mapped[UUID] = mapped_column(nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
primary_key=True, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPPlatformSubject(Base):
|
||
"""Canonical principal mapping(ADR-115)"""
|
||
|
||
__tablename__ = "awooop_platform_subjects"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"project_id", "channel_type", "channel_user_id",
|
||
name="uq_platform_subject",
|
||
),
|
||
CheckConstraint(
|
||
"jsonb_typeof(roles) = 'array'", name="chk_roles_array"
|
||
),
|
||
Index(
|
||
"idx_platform_subjects_lookup",
|
||
"project_id", "channel_type", "channel_user_id",
|
||
),
|
||
Index(
|
||
"idx_platform_subjects_resolve",
|
||
"project_id", "platform_subject_id",
|
||
),
|
||
Index(
|
||
"idx_platform_subjects_last_seen",
|
||
"project_id", "last_seen_at",
|
||
),
|
||
)
|
||
|
||
subject_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
channel_user_id: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
platform_subject_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
display_name: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
roles: Mapped[list[str]] = mapped_column(
|
||
JSONB, nullable=False, server_default=text("'[]'::jsonb")
|
||
)
|
||
first_seen_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
last_seen_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPProjectMigrationState(Base):
|
||
"""Strangler Fig migration state per project × capability(ADR-106 遷移追蹤)"""
|
||
|
||
__tablename__ = "awooop_project_migration_state"
|
||
__table_args__ = (
|
||
UniqueConstraint("project_id", "capability", name="uq_project_capability"),
|
||
CheckConstraint(
|
||
"capability IN ("
|
||
"'run_execution','contract_governance',"
|
||
"'budget_tracking','principal_mapping')",
|
||
name="chk_capability",
|
||
),
|
||
CheckConstraint(
|
||
"current_phase IN ("
|
||
"'legacy_awoooi_default','shadow','canary',"
|
||
"'read_only','suggest','auto_remediate')",
|
||
name="chk_phase",
|
||
),
|
||
)
|
||
|
||
state_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
capability: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
current_phase: Mapped[str] = mapped_column(
|
||
String(32), nullable=False, default="legacy_awoooi_default"
|
||
)
|
||
phase_entered_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Phase 4: Run State Machine(ADR-114/ADR-119)
|
||
# 2026-05-04 ogt + Claude Sonnet 4.6
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
class AwoooPRunState(Base):
|
||
"""Run FSM 主表(SKIP LOCKED worker lease,ADR-114)"""
|
||
|
||
__tablename__ = "awooop_run_state"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"state IN ("
|
||
"'pending','running','waiting_tool',"
|
||
"'waiting_approval','completed','failed','cancelled','timeout')",
|
||
name="chk_run_state",
|
||
),
|
||
Index("idx_run_state_pending", "project_id", "created_at",
|
||
postgresql_where=text("state = 'pending' AND lease_until IS NULL")),
|
||
Index("idx_run_state_stale", "lease_until",
|
||
postgresql_where=text("state = 'running' AND lease_until IS NOT NULL")),
|
||
Index("idx_run_state_project_timeline", "project_id", "created_at"),
|
||
Index("idx_run_state_trace_id", "trace_id",
|
||
postgresql_where=text("trace_id IS NOT NULL")),
|
||
)
|
||
|
||
run_id: Mapped[UUID] = mapped_column(primary_key=True)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
|
||
)
|
||
agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
state: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
|
||
lease_until: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
heartbeat_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
worker_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
attempt_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
max_attempts: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=3)
|
||
trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
trigger_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
trigger_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
is_shadow: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
input_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
output_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
cost_usd: Mapped[Decimal] = mapped_column(
|
||
Numeric(10, 4), nullable=False, default=Decimal("0.0000")
|
||
)
|
||
step_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
error_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
started_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
timeout_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
|
||
|
||
class AwoooPRunStepJournal(Base):
|
||
"""SAGA step journal(ADR-119)— 每個 tool call 獨立記錄"""
|
||
|
||
__tablename__ = "awooop_run_step_journal"
|
||
__table_args__ = (
|
||
UniqueConstraint("run_id", "step_seq", name="uix_run_step_seq"),
|
||
CheckConstraint(
|
||
"result_status IN ('pending','success','failed','compensated')",
|
||
name="chk_step_result_status",
|
||
),
|
||
Index("idx_run_step_run_id", "run_id", "step_seq"),
|
||
)
|
||
|
||
step_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
run_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_run_state.run_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
step_seq: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
||
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
mcp_gateway_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
compensation_json: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
|
||
result_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
|
||
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
was_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
block_reason: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
|
||
|
||
class AwoooPRunIdempotency(Base):
|
||
"""Run 去重冪等表(ADR-114)— (project_id, channel_type, provider_event_id) → run_id"""
|
||
|
||
__tablename__ = "awooop_run_idempotency"
|
||
__table_args__ = (
|
||
UniqueConstraint(
|
||
"project_id", "channel_type", "provider_event_id",
|
||
name="uix_run_idempotency_key",
|
||
),
|
||
Index("idx_run_idempotency_run_id", "run_id"),
|
||
)
|
||
|
||
idempotency_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
run_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_run_state.run_id"), nullable=False
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# Phase 5: MCP Gateway 四表(ADR-116/ADR-118,2026-05-04)
|
||
# =============================================================================
|
||
|
||
|
||
class AwoooPMcpToolRegistry(Base):
|
||
"""MCP Tool 白名單(Gate 3: Tool)"""
|
||
|
||
__tablename__ = "awooop_mcp_tool_registry"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"tool_type IN ('builtin','mcp_server','custom')",
|
||
name="chk_tool_type",
|
||
),
|
||
CheckConstraint(
|
||
"jsonb_typeof(allowed_scopes) = 'array'",
|
||
name="chk_allowed_scopes_array",
|
||
),
|
||
UniqueConstraint("project_id", "tool_name", name="uix_tool_registry_project_name"),
|
||
Index("idx_mcp_tool_registry_project", "project_id", "is_active"),
|
||
)
|
||
|
||
tool_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
tool_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
allowed_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||
environment_tags: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPMcpGrant(Base):
|
||
"""Agent × Tool 授權記錄(Gate 2 + Gate 3)"""
|
||
|
||
__tablename__ = "awooop_mcp_grants"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"jsonb_typeof(granted_scopes) = 'array'",
|
||
name="chk_grant_scopes_array",
|
||
),
|
||
CheckConstraint(
|
||
"(is_revoked = FALSE AND revoked_at IS NULL AND revoked_by IS NULL)"
|
||
" OR (is_revoked = TRUE AND revoked_at IS NOT NULL)",
|
||
name="chk_revoke_consistency",
|
||
),
|
||
UniqueConstraint("project_id", "agent_id", "tool_id", name="uix_mcp_grant_agent_tool"),
|
||
Index(
|
||
"idx_mcp_grants_lookup", "project_id", "agent_id", "tool_id",
|
||
postgresql_where=text("is_revoked = FALSE"),
|
||
),
|
||
)
|
||
|
||
grant_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
tool_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
granted_by: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
granted_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||
expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
is_revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
revoked_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
revoked_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPMcpCredentialRef(Base):
|
||
"""k8s Secret 參照(ADR-118 credential isolation)— 只存路徑,不存明文"""
|
||
|
||
__tablename__ = "awooop_mcp_credential_refs"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
r"k8s_secret_ref ~ '^[a-z0-9-]+/[a-z0-9-]+#[a-zA-Z0-9_-]+$'",
|
||
name="chk_k8s_ref_format",
|
||
),
|
||
CheckConstraint(
|
||
r"value_sha256 IS NULL OR value_sha256 ~ '^[0-9a-f]{64}$'",
|
||
name="chk_value_sha256_hex",
|
||
),
|
||
UniqueConstraint("tool_id", "k8s_secret_ref", name="uix_credential_ref_tool"),
|
||
Index("idx_mcp_cred_refs_tool", "tool_id", postgresql_where=text("is_active = TRUE")),
|
||
)
|
||
|
||
ref_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
tool_id: Mapped[UUID] = mapped_column(
|
||
ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
k8s_secret_ref: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
value_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
rotated_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
|
||
|
||
class AwoooPMcpGatewayAudit(Base):
|
||
"""MCP Gateway call 稽核日誌(ADR-116 P1-09)"""
|
||
|
||
__tablename__ = "awooop_mcp_gateway_audit"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"result_status IN ('success','blocked','failed','timeout')",
|
||
name="chk_gateway_result_status",
|
||
),
|
||
CheckConstraint(
|
||
"block_gate IS NULL OR (block_gate >= 1 AND block_gate <= 5)",
|
||
name="chk_block_gate_range",
|
||
),
|
||
Index("idx_mcp_audit_run", "project_id", "run_id", "created_at"),
|
||
Index(
|
||
"idx_mcp_audit_blocked", "project_id", "block_gate", "created_at",
|
||
postgresql_where=text("result_status = 'blocked'"),
|
||
),
|
||
)
|
||
|
||
call_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
run_id: Mapped[UUID | None] = mapped_column(nullable=True)
|
||
trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
agent_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
tool_id: Mapped[UUID | None] = mapped_column(
|
||
ForeignKey("awooop_mcp_tool_registry.tool_id"), nullable=True
|
||
)
|
||
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
credential_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
gate_result: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
result_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
block_gate: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
|
||
block_reason: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# Phase 7: Channel Hub 雙表(ADR-106 channel_event family,2026-05-04)
|
||
# =============================================================================
|
||
|
||
|
||
class AwoooPConversationEvent(Base):
|
||
"""入站 Channel Event 鏡像(Telegram/LINE inbound,不儲存明文)"""
|
||
|
||
__tablename__ = "awooop_conversation_event"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"channel_type IN ('telegram','line','slack','api','internal')",
|
||
name="chk_conv_event_channel_type",
|
||
),
|
||
CheckConstraint(
|
||
"content_type IN ('text','photo','document','command','callback_query')",
|
||
name="chk_conv_event_content_type",
|
||
),
|
||
UniqueConstraint(
|
||
"project_id", "channel_type", "provider_event_id",
|
||
name="uix_conv_event_dedup",
|
||
),
|
||
Index("idx_conv_event_run", "project_id", "run_id", "received_at"),
|
||
Index("idx_conv_event_subject", "project_id", "platform_subject_id", "received_at"),
|
||
)
|
||
|
||
event_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
platform_subject_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
channel_user_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
run_id: Mapped[UUID | None] = mapped_column(nullable=True)
|
||
content_type: Mapped[str] = mapped_column(String(32), nullable=False, default="text")
|
||
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
redaction_version: Mapped[str] = mapped_column(
|
||
String(32), nullable=False, server_default=text("'audit_sink_v1'")
|
||
)
|
||
source_envelope: Mapped[dict[str, Any]] = mapped_column(
|
||
JSONB, nullable=False, server_default=text("'{}'::jsonb")
|
||
)
|
||
attachment_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
is_duplicate: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
provider_ts: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
received_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
|
||
|
||
class AwoooPOutboundMessage(Base):
|
||
"""出站訊息記錄(interim/final/approval_request + shadow status)"""
|
||
|
||
__tablename__ = "awooop_outbound_message"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"channel_type IN ('telegram','line','slack','api','internal')",
|
||
name="chk_outbound_channel_type",
|
||
),
|
||
CheckConstraint(
|
||
"message_type IN ('interim','final','error','approval_request')",
|
||
name="chk_outbound_message_type",
|
||
),
|
||
CheckConstraint(
|
||
"send_status IN ('pending','sent','failed','shadow')",
|
||
name="chk_outbound_send_status",
|
||
),
|
||
Index("idx_outbound_msg_run", "project_id", "run_id", "queued_at"),
|
||
Index(
|
||
"idx_outbound_msg_pending", "project_id", "channel_type", "queued_at",
|
||
postgresql_where=text("send_status = 'pending'"),
|
||
),
|
||
)
|
||
|
||
message_id: Mapped[UUID] = mapped_column(
|
||
primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
project_id: Mapped[str] = mapped_column(
|
||
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
run_id: Mapped[UUID] = mapped_column(nullable=False)
|
||
conversation_event_id: Mapped[UUID | None] = mapped_column(nullable=True)
|
||
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
channel_chat_id: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
message_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||
content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
redaction_version: Mapped[str] = mapped_column(
|
||
String(32), nullable=False, server_default=text("'audit_sink_v1'")
|
||
)
|
||
source_envelope: Mapped[dict[str, Any]] = mapped_column(
|
||
JSONB, nullable=False, server_default=text("'{}'::jsonb")
|
||
)
|
||
provider_message_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
send_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
|
||
send_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
queued_at: Mapped[datetime] = mapped_column(
|
||
nullable=False, server_default=text("NOW()")
|
||
)
|
||
sent_at: Mapped[datetime | None] = mapped_column(nullable=True)
|
||
triggered_by_state: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
waiting_since: Mapped[datetime | None] = mapped_column(nullable=True)
|