mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
feat(agent): add host policy foundation (#6273)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""MoviePilot Agent 宿主策略公共内部入口。"""
|
||||
|
||||
from app.agent.policy.contracts import (
|
||||
ActionEffect,
|
||||
ActionPolicy,
|
||||
AuthSource,
|
||||
ConfirmationMode,
|
||||
ExecutionOutcome,
|
||||
ExecutionReceipt,
|
||||
MigrationState,
|
||||
PolicyDecision,
|
||||
PolicyObservation,
|
||||
PolicyPrincipal,
|
||||
PrincipalRole,
|
||||
PrincipalType,
|
||||
RecoveryMode,
|
||||
ResultSensitivity,
|
||||
ToolInvocation,
|
||||
ToolOrigin,
|
||||
ToolPolicyContext,
|
||||
)
|
||||
from app.agent.policy.orchestrator import (
|
||||
DEFAULT_TOOL_POLICY_ORCHESTRATOR,
|
||||
AgentToolPolicyOrchestrator,
|
||||
call_policy_hook,
|
||||
)
|
||||
from app.agent.policy.registry import DEFAULT_TOOL_POLICY_REGISTRY, ToolPolicyRegistry
|
||||
from app.agent.policy.sanitizer import (
|
||||
REDACTED_VALUE,
|
||||
sanitize_for_host,
|
||||
stable_type_name,
|
||||
summarize_error,
|
||||
summarize_input,
|
||||
summarize_result,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ActionEffect",
|
||||
"ActionPolicy",
|
||||
"AgentToolPolicyOrchestrator",
|
||||
"AuthSource",
|
||||
"ConfirmationMode",
|
||||
"DEFAULT_TOOL_POLICY_ORCHESTRATOR",
|
||||
"DEFAULT_TOOL_POLICY_REGISTRY",
|
||||
"ExecutionOutcome",
|
||||
"ExecutionReceipt",
|
||||
"MigrationState",
|
||||
"PolicyDecision",
|
||||
"PolicyObservation",
|
||||
"PolicyPrincipal",
|
||||
"PrincipalRole",
|
||||
"PrincipalType",
|
||||
"REDACTED_VALUE",
|
||||
"RecoveryMode",
|
||||
"ResultSensitivity",
|
||||
"ToolInvocation",
|
||||
"ToolOrigin",
|
||||
"ToolPolicyContext",
|
||||
"ToolPolicyRegistry",
|
||||
"call_policy_hook",
|
||||
"sanitize_for_host",
|
||||
"stable_type_name",
|
||||
"summarize_error",
|
||||
"summarize_input",
|
||||
"summarize_result",
|
||||
]
|
||||
@@ -0,0 +1,247 @@
|
||||
"""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):
|
||||
"""P1-G1 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 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:
|
||||
"""P1-G1 的非持久化脱敏回执 envelope。"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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",
|
||||
]
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Agent 工具策略观测、脱敏回执与共享执行边界。"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Mapping, Optional, TypeVar
|
||||
|
||||
from langchain_core.messages import ToolMessage
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.agent.policy.contracts import (
|
||||
ExecutionOutcome,
|
||||
ExecutionReceipt,
|
||||
MigrationState,
|
||||
PolicyDecision,
|
||||
PolicyObservation,
|
||||
ToolInvocation,
|
||||
ToolPolicyContext,
|
||||
)
|
||||
from app.agent.policy.registry import DEFAULT_TOOL_POLICY_REGISTRY, ToolPolicyRegistry
|
||||
from app.agent.policy.sanitizer import (
|
||||
stable_type_name,
|
||||
summarize_error,
|
||||
summarize_input,
|
||||
summarize_result,
|
||||
)
|
||||
from app.log import logger
|
||||
|
||||
|
||||
_HookResult = TypeVar("_HookResult")
|
||||
|
||||
|
||||
def call_policy_hook(
|
||||
phase: str,
|
||||
hook: Callable[..., _HookResult],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Optional[_HookResult]:
|
||||
"""以 fail-open 方式调用 P1-G1 观测 hook,故障只记录稳定类型。"""
|
||||
try:
|
||||
return hook(*args, **kwargs)
|
||||
except Exception as error:
|
||||
try:
|
||||
logger.warning(
|
||||
f"Agent工具策略观测失败: phase={phase}, "
|
||||
f"error_type={stable_type_name(error)}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_policy_arguments(tool: Any, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""为策略生成 Pydantic 规范化副本,不改变真实执行参数。"""
|
||||
raw_arguments = dict(arguments or {})
|
||||
args_schema = getattr(tool, "args_schema", None)
|
||||
if not args_schema:
|
||||
return raw_arguments
|
||||
try:
|
||||
validated = args_schema.model_validate(raw_arguments)
|
||||
return validated.model_dump(mode="json")
|
||||
except (AttributeError, TypeError, ValueError, ValidationError):
|
||||
# 实际 handler 仍负责既有参数错误语义;策略观测按原始值保守处理。
|
||||
return raw_arguments
|
||||
|
||||
|
||||
def _result_payload(result: Any) -> Any:
|
||||
"""从 LangChain 工具消息中提取模型可见结果供脱敏摘要使用。"""
|
||||
if isinstance(result, ToolMessage):
|
||||
return result.content
|
||||
return result
|
||||
|
||||
|
||||
class AgentToolPolicyOrchestrator:
|
||||
"""让 Agent middleware 与 direct manager 复用同一策略生命周期。"""
|
||||
|
||||
def __init__(self, registry: ToolPolicyRegistry = DEFAULT_TOOL_POLICY_REGISTRY) -> None:
|
||||
"""绑定固定工具迁移注册表。"""
|
||||
self.registry = registry
|
||||
|
||||
def start(
|
||||
self,
|
||||
*,
|
||||
context: ToolPolicyContext,
|
||||
tool: Any,
|
||||
arguments: Mapping[str, Any],
|
||||
invocation_id: Optional[str] = None,
|
||||
) -> PolicyObservation:
|
||||
"""解析调用策略,并创建不影响现有 allow 行为的观测对象。"""
|
||||
tool_name = str(getattr(tool, "name", None) or "unknown_tool")
|
||||
normalized_arguments = _normalize_policy_arguments(tool, arguments)
|
||||
policy = self.registry.resolve(
|
||||
tool_name=tool_name,
|
||||
arguments=normalized_arguments,
|
||||
requires_admin=bool(getattr(tool, "_require_admin", False)),
|
||||
)
|
||||
if policy.migration_state is MigrationState.LEGACY_SHADOW:
|
||||
decision = PolicyDecision(
|
||||
allowed=True,
|
||||
confirmation_required=False,
|
||||
shadow=True,
|
||||
reason_code="legacy_shadow_allow",
|
||||
)
|
||||
else:
|
||||
decision = PolicyDecision(
|
||||
allowed=True,
|
||||
confirmation_required=False,
|
||||
shadow=False,
|
||||
reason_code="safe_read_allow",
|
||||
)
|
||||
invocation = ToolInvocation(
|
||||
invocation_id=invocation_id or uuid.uuid4().hex,
|
||||
tool_name=tool_name,
|
||||
arguments=normalized_arguments,
|
||||
principal=context.principal,
|
||||
session_id=context.session_id,
|
||||
origin=context.origin,
|
||||
channel=context.channel,
|
||||
source=context.source,
|
||||
)
|
||||
input_summary = summarize_input(normalized_arguments)
|
||||
observation = PolicyObservation(
|
||||
invocation=invocation,
|
||||
policy=policy,
|
||||
decision=decision,
|
||||
input_summary=input_summary,
|
||||
started_at=time.monotonic(),
|
||||
)
|
||||
logger.debug(
|
||||
f"Agent工具策略: tool={tool_name}, origin={context.origin.value}, "
|
||||
f"decision={decision.reason_code}, input={input_summary}"
|
||||
)
|
||||
return observation
|
||||
|
||||
@staticmethod
|
||||
def finish(observation: PolicyObservation, result: Any) -> ExecutionReceipt:
|
||||
"""生成成功回执 envelope,并只记录脱敏结果摘要。"""
|
||||
result_summary = summarize_result(_result_payload(result))
|
||||
receipt = ExecutionReceipt(
|
||||
invocation_id=observation.invocation.invocation_id,
|
||||
tool_name=observation.invocation.tool_name,
|
||||
origin=observation.invocation.origin,
|
||||
decision=observation.decision,
|
||||
outcome=ExecutionOutcome.SUCCEEDED,
|
||||
input_summary=observation.input_summary,
|
||||
result_summary=result_summary,
|
||||
duration_ms=max(
|
||||
0,
|
||||
int((time.monotonic() - observation.started_at) * 1000),
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
f"Agent工具执行完成: tool={receipt.tool_name}, "
|
||||
f"origin={receipt.origin.value}, shadow={receipt.decision.shadow}, "
|
||||
f"duration_ms={receipt.duration_ms}, result={result_summary}"
|
||||
)
|
||||
return receipt
|
||||
|
||||
@staticmethod
|
||||
def fail(observation: PolicyObservation, error: BaseException) -> ExecutionReceipt:
|
||||
"""生成失败回执 envelope,不把异常中的凭据写入日志。"""
|
||||
error_summary = summarize_error(error)
|
||||
receipt = ExecutionReceipt(
|
||||
invocation_id=observation.invocation.invocation_id,
|
||||
tool_name=observation.invocation.tool_name,
|
||||
origin=observation.invocation.origin,
|
||||
decision=observation.decision,
|
||||
outcome=ExecutionOutcome.FAILED,
|
||||
input_summary=observation.input_summary,
|
||||
error_summary=error_summary,
|
||||
duration_ms=max(
|
||||
0,
|
||||
int((time.monotonic() - observation.started_at) * 1000),
|
||||
),
|
||||
)
|
||||
logger.error(
|
||||
f"Agent工具执行失败: tool={receipt.tool_name}, "
|
||||
f"origin={receipt.origin.value}, shadow={receipt.decision.shadow}, "
|
||||
f"duration_ms={receipt.duration_ms}, error={error_summary}"
|
||||
)
|
||||
return receipt
|
||||
|
||||
|
||||
DEFAULT_TOOL_POLICY_ORCHESTRATOR = AgentToolPolicyOrchestrator()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentToolPolicyOrchestrator",
|
||||
"DEFAULT_TOOL_POLICY_ORCHESTRATOR",
|
||||
"call_policy_hook",
|
||||
]
|
||||
@@ -0,0 +1,181 @@
|
||||
"""固定工具迁移注册表与参数级策略解析。"""
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from app.agent.policy.contracts import (
|
||||
ActionEffect,
|
||||
ActionPolicy,
|
||||
ConfirmationMode,
|
||||
MigrationState,
|
||||
PrincipalRole,
|
||||
RecoveryMode,
|
||||
ResultSensitivity,
|
||||
)
|
||||
|
||||
|
||||
# 这些读取已具备清晰的无副作用语义,用于证明新宿主边界不会改变正常结果。
|
||||
SAFE_READ_TOOL_NAMES = frozenset(
|
||||
{
|
||||
"list_slash_commands",
|
||||
"query_installed_plugins",
|
||||
"query_personas",
|
||||
"query_schedulers",
|
||||
"query_workflows",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 其余固定工具先显式处于兼容观测状态,待领域叶子 Goal 逐个迁移。
|
||||
LEGACY_SHADOW_TOOL_NAMES = frozenset(
|
||||
{
|
||||
"add_custom_filter_rule",
|
||||
"add_download_tasks",
|
||||
"add_rule_group",
|
||||
"add_subscribe",
|
||||
"ask_user_choice",
|
||||
"browse_webpage",
|
||||
"create_agent_task",
|
||||
"delete_agent_task",
|
||||
"delete_custom_filter_rule",
|
||||
"delete_download_history",
|
||||
"delete_download_tasks",
|
||||
"delete_rule_group",
|
||||
"delete_subscribe",
|
||||
"delete_transfer_history",
|
||||
"edit_file",
|
||||
"execute_command",
|
||||
"get_recommendations",
|
||||
"get_search_results",
|
||||
"install_plugin",
|
||||
"list_directory",
|
||||
"query_agent_tasks",
|
||||
"query_builtin_filter_rules",
|
||||
"query_custom_filter_rules",
|
||||
"query_custom_identifiers",
|
||||
"query_directory_settings",
|
||||
"query_doctor_report",
|
||||
"query_download_tasks",
|
||||
"query_downloaders",
|
||||
"query_episode_schedule",
|
||||
"query_library_exists",
|
||||
"query_library_latest",
|
||||
"query_market_plugins",
|
||||
"query_media_detail",
|
||||
"query_plugin_capabilities",
|
||||
"query_plugin_config",
|
||||
"query_plugin_data",
|
||||
"query_popular_subscribes",
|
||||
"query_rule_groups",
|
||||
"query_site_userdata",
|
||||
"query_sites",
|
||||
"query_subscribe_history",
|
||||
"query_subscribe_shares",
|
||||
"query_subscribes",
|
||||
"query_system_settings",
|
||||
"query_transfer_history",
|
||||
"read_file",
|
||||
"recognize_captcha",
|
||||
"recognize_media",
|
||||
"reload_plugin",
|
||||
"run_agent_task",
|
||||
"run_scheduler",
|
||||
"run_slash_command",
|
||||
"run_workflow",
|
||||
"scrape_metadata",
|
||||
"search_media",
|
||||
"search_person",
|
||||
"search_person_credits",
|
||||
"search_subscribe",
|
||||
"search_torrents",
|
||||
"search_web",
|
||||
"send_local_file",
|
||||
"send_message",
|
||||
"send_voice_message",
|
||||
"switch_persona",
|
||||
"test_site",
|
||||
"transfer_file",
|
||||
"uninstall_plugin",
|
||||
"update_agent_task",
|
||||
"update_custom_filter_rule",
|
||||
"update_custom_identifiers",
|
||||
"update_download_tasks",
|
||||
"update_persona_definition",
|
||||
"update_plugin_config",
|
||||
"update_rule_group",
|
||||
"update_site",
|
||||
"update_site_cookie",
|
||||
"update_subscribe",
|
||||
"update_system_settings",
|
||||
"write_file",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ToolPolicyRegistry:
|
||||
"""解析固定和动态工具的 P1-G1 迁移策略。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
safe_read_tool_names: frozenset[str] = SAFE_READ_TOOL_NAMES,
|
||||
legacy_shadow_tool_names: frozenset[str] = LEGACY_SHADOW_TOOL_NAMES,
|
||||
) -> None:
|
||||
"""建立互斥的固定工具迁移表。"""
|
||||
overlap = safe_read_tool_names & legacy_shadow_tool_names
|
||||
if overlap:
|
||||
raise ValueError(f"工具策略迁移表存在重复项: {sorted(overlap)}")
|
||||
self._safe_read_tool_names = safe_read_tool_names
|
||||
self._legacy_shadow_tool_names = legacy_shadow_tool_names
|
||||
|
||||
@property
|
||||
def builtin_tool_names(self) -> set[str]:
|
||||
"""返回注册表覆盖的全部固定工具名。"""
|
||||
return set(self._safe_read_tool_names | self._legacy_shadow_tool_names)
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
*,
|
||||
tool_name: str,
|
||||
arguments: Mapping[str, Any],
|
||||
requires_admin: bool,
|
||||
) -> ActionPolicy:
|
||||
"""根据工具名和宿主权限声明解析当前迁移策略。"""
|
||||
del arguments # 参数级迁移由后续领域 Goal 逐项加入。
|
||||
required_role = (
|
||||
PrincipalRole.SYSTEM_ADMIN if requires_admin else PrincipalRole.USER
|
||||
)
|
||||
if tool_name in self._safe_read_tool_names:
|
||||
return ActionPolicy(
|
||||
effect=ActionEffect.SAFE_READ,
|
||||
required_role=required_role,
|
||||
confirmation=ConfirmationMode.NONE,
|
||||
recovery=RecoveryMode.NONE,
|
||||
result_sensitivity=ResultSensitivity.NORMAL,
|
||||
# 角色门禁仍可能异步识别渠道管理员;G1 不复制旧授权事实源。
|
||||
migration_state=(
|
||||
MigrationState.LEGACY_SHADOW
|
||||
if requires_admin
|
||||
else MigrationState.ENFORCED
|
||||
),
|
||||
)
|
||||
|
||||
# 固定未迁移工具和动态工具都保持现有执行能力,但不得被视为安全读取。
|
||||
return ActionPolicy(
|
||||
effect=ActionEffect.UNKNOWN,
|
||||
required_role=required_role,
|
||||
confirmation=ConfirmationMode.REQUIRED,
|
||||
recovery=RecoveryMode.MANUAL_ONLY,
|
||||
result_sensitivity=ResultSensitivity.UNKNOWN,
|
||||
migration_state=MigrationState.LEGACY_SHADOW,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_TOOL_POLICY_REGISTRY = ToolPolicyRegistry()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_TOOL_POLICY_REGISTRY",
|
||||
"LEGACY_SHADOW_TOOL_NAMES",
|
||||
"SAFE_READ_TOOL_NAMES",
|
||||
"ToolPolicyRegistry",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
"""Agent 设置工具与宿主回执共用的敏感字段身份判定。"""
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
_MAX_FIELD_NAME_CHARS = 256
|
||||
_ACRONYM_BOUNDARY_PATTERN = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])")
|
||||
_CAMEL_CASE_BOUNDARY_PATTERN = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
|
||||
_SECRET_FIELD_NAMES = frozenset(
|
||||
{
|
||||
"access_token",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"api_token",
|
||||
"auth_header",
|
||||
"authorization",
|
||||
"client_secret",
|
||||
"cookie",
|
||||
"passkey",
|
||||
"passwd",
|
||||
"password",
|
||||
"private_key",
|
||||
"pwd",
|
||||
"refresh_token",
|
||||
"secret",
|
||||
"secret_access_key",
|
||||
"secret_key",
|
||||
"token",
|
||||
}
|
||||
)
|
||||
_SECRET_FIELD_ENDINGS = tuple(f"_{name}" for name in _SECRET_FIELD_NAMES)
|
||||
_SECRET_SETTING_NAMES = frozenset(
|
||||
{
|
||||
# CookieCloud 的用户 key 没有类型后缀,但与密码共同构成端到端加密凭据。
|
||||
"cookiecloud_key",
|
||||
}
|
||||
)
|
||||
_SECRET_SETTING_ENDINGS = (
|
||||
"_encrypt_key",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_field_name(value: Any) -> str:
|
||||
"""将短字段名规范化为 snake_case,非字符串不参与身份推导。"""
|
||||
if type(value) is not str:
|
||||
return ""
|
||||
text = value.strip()
|
||||
if len(text) > _MAX_FIELD_NAME_CHARS:
|
||||
text = text[-_MAX_FIELD_NAME_CHARS:]
|
||||
text = _ACRONYM_BOUNDARY_PATTERN.sub("_", text)
|
||||
text = _CAMEL_CASE_BOUNDARY_PATTERN.sub("_", text)
|
||||
return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
|
||||
|
||||
def is_secret_setting_key(key: Any) -> bool:
|
||||
"""按完整字段或类型后缀识别凭据,避免误伤 token 统计与过期配置。"""
|
||||
normalized = _normalize_field_name(key)
|
||||
if not normalized:
|
||||
return False
|
||||
return (
|
||||
normalized in _SECRET_FIELD_NAMES
|
||||
or normalized in _SECRET_SETTING_NAMES
|
||||
or normalized.endswith(_SECRET_FIELD_ENDINGS)
|
||||
or normalized.endswith(_SECRET_SETTING_ENDINGS)
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["is_secret_setting_key"]
|
||||
Reference in New Issue
Block a user