Files
awoooi/apps/api/src/db/awooop_models.py
ogt 6cf8429d17
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m32s
CD Pipeline / build-and-deploy (push) Successful in 8m2s
CD Pipeline / post-deploy-checks (push) Successful in 2m22s
fix(automation): enforce durable alert closure
Restore D037 durable incident readback without weakening database failure semantics.

Fence Agent99 dispatch and terminal learning to canonical identities, durable leases, verifier receipts, and reconciler-owned checkpoints.

Drain backup and restore legacy receipts in bounded cohorts with strict transport, claim, projection, and no-false-green controls.

Keep SSH service refusal visible while separating it from broker network-policy reachability.
2026-07-14 09:40:53 +08:00

710 lines
30 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.
"""
AwoooP Control Plane Models
============================
Phase 1 新表:六合約 control plane、tenant 隔離、principal mapping。
ADR-111~1182026-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 bootstrapADR-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-onlyADR-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 pointerADR-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 invalidationADR-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 keyADR-114partitioned 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 PKpartition 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 mappingADR-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 × capabilityADR-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 MachineADR-114/ADR-119
# 2026-05-04 ogt + Claude Sonnet 4.6
# ─────────────────────────────────────────────────────────────────────────────
class AwoooPRunState(Base):
"""Run FSM 主表SKIP LOCKED worker leaseADR-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_retry_due", "project_id", "next_attempt_at",
postgresql_where=text("state = 'pending' AND next_attempt_at IS NOT 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)
next_attempt_at: 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 journalADR-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-1182026-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 family2026-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"),
Index("idx_conv_event_recent", "project_id", "channel_type", "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)