"""MoviePilot Agent 宿主策略的内部契约。""" from dataclasses import dataclass, field from enum import Enum from typing import Any, Mapping, MutableMapping, Optional class ToolOrigin(str, Enum): """工具调用的宿主可信入口。""" AGENT_INTERACTIVE = "agent_interactive" AGENT_API = "agent_api" OPERATOR_DIRECT = "operator_direct" BACKGROUND = "background" SUBAGENT = "subagent" class PrincipalType(str, Enum): """调用主体类型,用于区分人、管理员集成和内部运行时。""" HUMAN = "human" SYSTEM_ADMIN_INTEGRATION = "system_admin_integration" SCOPED_AGENT = "scoped_agent" BACKGROUND = "background" SUBAGENT = "subagent" class AuthSource(str, Enum): """主体身份的宿主认证来源。""" CHANNEL = "channel" WEB_SESSION = "web_session" API_TOKEN = "api_token" INTERNAL = "internal" AGENT_TOKEN = "agent_token" class PrincipalRole(str, Enum): """策略授权使用的角色层级。""" USER = "user" CHANNEL_ADMIN = "channel_admin" SYSTEM_ADMIN = "system_admin" SYSTEM_INTERNAL = "system_internal" class ActionEffect(str, Enum): """工具调用的实际副作用类别。""" SAFE_READ = "safe_read" SENSITIVE_READ = "sensitive_read" REVERSIBLE_WRITE = "reversible_write" DESTRUCTIVE_WRITE = "destructive_write" EXTERNAL_SIDE_EFFECT = "external_side_effect" ARBITRARY_EXECUTION = "arbitrary_execution" UNKNOWN = "unknown" class ConfirmationMode(str, Enum): """动作在完成授权后所需的确认方式。""" NONE = "none" REQUIRED = "required" UNSUPPORTED = "unsupported" class RecoveryMode(str, Enum): """动作可提供的执行恢复保证。""" NONE = "none" TRANSACTION = "transaction" BEFORE_STATE = "before_state" RECOVERABLE_DELETE = "recoverable_delete" IDEMPOTENT = "idempotent" RECONCILE = "reconcile" MANUAL_ONLY = "manual_only" class ResultSensitivity(str, Enum): """工具结果进入模型、记忆和日志时的敏感等级。""" NORMAL = "normal" PRIVATE = "private" SECRET = "secret" UNKNOWN = "unknown" class MigrationState(str, Enum): """工具策略当前采用宿主强制还是兼容观测。""" ENFORCED = "enforced" LEGACY_SHADOW = "legacy_shadow" class ExecutionOutcome(str, Enum): """工具 handler 观测终态;成功不代表业务授权或副作用已完成。""" SUCCEEDED = "succeeded" FAILED = "failed" @dataclass(frozen=True) class PolicyPrincipal: """由可信入口建立、不可由工具参数覆盖的调用主体。""" principal_id: str principal_type: PrincipalType auth_source: AuthSource role: PrincipalRole scopes: tuple[str, ...] = () @dataclass(frozen=True) class ToolInvocation: """一次进入宿主策略层的规范化工具调用。""" invocation_id: str tool_name: str arguments: Mapping[str, Any] principal: PolicyPrincipal session_id: str origin: ToolOrigin channel: Optional[str] = None source: Optional[str] = None @dataclass(frozen=True) class ToolRevision: """记录目录项绑定的工具实现、工厂和插件目录版本。""" implementation: str factory: str plugin: str @dataclass(frozen=True) class ActionPolicy: """参数级动作策略及其兼容迁移状态。""" effect: ActionEffect required_role: PrincipalRole confirmation: ConfirmationMode recovery: RecoveryMode result_sensitivity: ResultSensitivity migration_state: MigrationState policy_version: str = "p1-g1-v1" interactive_allowed: bool = True machine_allowed: bool = True background_allowed: bool = True subagent_allowed: bool = True @dataclass(frozen=True) class PolicyDecision: """宿主策略层决定;shadow allow 仅表示新策略不拦截,旧门禁仍是授权事实源。""" allowed: bool confirmation_required: bool shadow: bool reason_code: str @dataclass(frozen=True) class PolicyObservation: """调用开始时生成、供完成或失败回执复用的观测对象。""" invocation: ToolInvocation policy: ActionPolicy decision: PolicyDecision input_summary: str started_at: float @dataclass(frozen=True) class ExecutionReceipt: """工具策略生命周期生成的非持久化脱敏回执。""" invocation_id: str tool_name: str origin: ToolOrigin decision: PolicyDecision outcome: ExecutionOutcome input_summary: str result_summary: Optional[str] = None error_summary: Optional[str] = None duration_ms: int = 0 external_may_continue: bool = False # 中断后外部操作仍可能继续,不能视为已停止。 needs_reconcile: bool = False # 调用方需查询外部实际状态后再决定补偿或重试。 @dataclass(frozen=True) class ToolPolicyContext: """宿主入口上下文;管理员状态引用会随缓存图的每轮执行刷新。""" session_id: str user_id: str origin: ToolOrigin principal_type: PrincipalType auth_source: AuthSource agent_context: MutableMapping[str, Any] = field(repr=False, compare=False) channel: Optional[str] = None source: Optional[str] = None @property def principal(self) -> PolicyPrincipal: """根据当前宿主上下文生成本次调用主体。""" if self.principal_type in {PrincipalType.BACKGROUND, PrincipalType.SUBAGENT}: default_role = PrincipalRole.SYSTEM_INTERNAL else: default_role = PrincipalRole.USER role = ( PrincipalRole.SYSTEM_ADMIN if bool(self.agent_context.get("is_admin")) else default_role ) raw_scopes = self.agent_context.get("policy_scopes") or () scopes = tuple(str(scope) for scope in raw_scopes if scope) return PolicyPrincipal( principal_id=str(self.user_id or self.principal_type.value), principal_type=self.principal_type, auth_source=self.auth_source, role=role, scopes=scopes, ) def for_subagent(self) -> "ToolPolicyContext": """保留用户与会话归属,并切换为子代理可信来源。""" return ToolPolicyContext( session_id=self.session_id, user_id=self.user_id, origin=ToolOrigin.SUBAGENT, principal_type=PrincipalType.SUBAGENT, auth_source=AuthSource.INTERNAL, agent_context=self.agent_context, channel=self.channel, source=self.source, ) __all__ = [ "ActionEffect", "ActionPolicy", "AuthSource", "ConfirmationMode", "ExecutionOutcome", "ExecutionReceipt", "MigrationState", "PolicyDecision", "PolicyObservation", "PolicyPrincipal", "PrincipalRole", "PrincipalType", "RecoveryMode", "ResultSensitivity", "ToolInvocation", "ToolOrigin", "ToolPolicyContext", "ToolRevision", ]