feat(plugin): 建立可信来源准入与安装恢复 (#6462)

This commit is contained in:
InfinityPacer
2026-08-26 07:52:00 +08:00
committed by GitHub
parent 3272f72823
commit 71db425c07
58 changed files with 11471 additions and 1493 deletions
+289 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import re
from collections.abc import Callable
from dataclasses import dataclass, replace
from datetime import datetime
from enum import StrEnum
@@ -295,7 +296,7 @@ def plan_legacy_plugin_identity(
trusted_key = None
basis = PluginBindingBasis.LEGACY_UNBOUND
bound_at = None
if market_availability is PluginMarketAvailability.AVAILABLE and official:
if official:
trusted_type = TrustedPluginSourceType.OFFICIAL
trusted_key = official[0].source_key
basis = PluginBindingBasis.OFFICIAL_DEFAULT
@@ -349,6 +350,21 @@ class PluginIdentityRepository(Protocol):
"""按 revision 条件暂存替换,并返回是否赢得竞争。"""
class PluginIdentityStore(Protocol):
"""组合根注入的独立来源身份读取与存量迁移端口。"""
def get(self, plugin_id: str) -> PluginIdentity | None:
"""读取一个物理插件的来源身份。"""
def compare_and_set(
self,
identity: PluginIdentity,
*,
expected_revision: int | None,
) -> PluginIdentity:
"""首次创建或按 revision 更新身份。"""
class PluginIdentityUnitOfWork(Protocol):
"""来源身份条件写使用的事务端口。"""
@@ -431,3 +447,275 @@ class WritePluginIdentityCommand:
except Exception:
self._unit_of_work.rollback()
raise
class ChangePluginIdentitySourceCommand:
"""以独立 CAS 合同提交一次明确的在线插件来源转换。"""
def __init__(
self,
repository: PluginIdentityRepository,
unit_of_work: PluginIdentityUnitOfWork,
) -> None:
"""保存仓储与事务所有者。"""
self._repository = repository
self._unit_of_work = unit_of_work
def execute(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> PluginIdentity:
"""只允许已有身份按精确 revision 切换到不同在线来源。"""
return _execute_identity_transition(
self._repository,
self._unit_of_work,
identity,
expected_revision=expected_revision,
validate=_validate_identity_source_change,
)
class BindOnlinePluginIdentityCommand:
"""以独立 CAS 合同为未绑定身份建立在线可信来源。"""
def __init__(
self,
repository: PluginIdentityRepository,
unit_of_work: PluginIdentityUnitOfWork,
) -> None:
"""保存仓储与事务所有者。"""
self._repository = repository
self._unit_of_work = unit_of_work
def execute(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> PluginIdentity:
"""只允许未绑定身份按精确 revision 首次绑定在线来源。"""
return _execute_identity_transition(
self._repository,
self._unit_of_work,
identity,
expected_revision=expected_revision,
validate=_validate_online_binding,
)
class BindLocalPluginIdentityCommand:
"""以独立 CAS 合同把存量未绑定身份转换为本地专属身份。"""
def __init__(
self,
repository: PluginIdentityRepository,
unit_of_work: PluginIdentityUnitOfWork,
) -> None:
"""保存仓储与事务所有者。"""
self._repository = repository
self._unit_of_work = unit_of_work
def execute(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> PluginIdentity:
"""只允许 legacy_unbound 身份按精确 revision 绑定本地载荷。"""
return _execute_identity_transition(
self._repository,
self._unit_of_work,
identity,
expected_revision=expected_revision,
validate=_validate_local_binding,
)
def _execute_identity_transition(
repository: PluginIdentityRepository,
unit_of_work: PluginIdentityUnitOfWork,
identity: PluginIdentity,
*,
expected_revision: int,
validate: Callable[[PluginIdentity, PluginIdentity], None],
) -> PluginIdentity:
"""在一个数据库事务中校验并提交专用身份转换。"""
try:
current = _prepare_identity_transition(
repository,
identity,
expected_revision=expected_revision,
)
validate(current, identity)
candidate = replace(
identity,
normalized_plugin_id=current.normalized_plugin_id,
revision=current.revision + 1,
created_at=current.created_at,
)
_stage_identity_transition(
repository,
unit_of_work,
candidate,
expected_revision=expected_revision,
)
return candidate
except Exception:
unit_of_work.rollback()
raise
def _prepare_identity_transition(
repository: PluginIdentityRepository,
identity: PluginIdentity,
*,
expected_revision: int,
) -> PluginIdentity:
"""读取转换基线并保证目标使用同一物理插件和精确 revision。"""
if expected_revision < 1:
raise PluginIdentityConflictError("插件来源身份 expected_revision 必须从 1 开始")
normalized_plugin_id = normalize_physical_plugin_id(identity.plugin_id)
current = repository.get(normalized_plugin_id)
if current is None or current.revision != expected_revision:
raise PluginIdentityConflictError(
f"插件 {identity.plugin_id} 的来源身份 revision 已被其他任务更新"
)
if (
identity.plugin_id != current.plugin_id
or identity.normalized_plugin_id != current.normalized_plugin_id
):
raise PluginIdentityConflictError(
"插件来源转换不能改变物理插件 ID"
)
if identity.created_at != current.created_at:
raise PluginIdentityConflictError(
"插件来源转换必须保留身份创建时间"
)
if identity.updated_at < current.updated_at:
raise PluginIdentityConflictError(
"插件身份更新时间不能早于已提交记录"
)
return current
def _validate_identity_source_change(
current: PluginIdentity,
candidate: PluginIdentity,
) -> None:
"""校验显式换源的来源、载荷和实际变化边界。"""
if candidate.binding_basis is not PluginBindingBasis.EXPLICIT_SOURCE_CHANGE:
raise PluginIdentityConflictError(
"显式换源目标必须使用 explicit_source_change 依据"
)
if candidate.trusted_source_type is TrustedPluginSourceType.UNKNOWN:
raise PluginIdentityConflictError("显式换源目标必须是在线可信来源")
if candidate.payload_source_type not in {
PluginPayloadSourceType.OFFICIAL,
PluginPayloadSourceType.THIRD_PARTY,
}:
raise PluginIdentityConflictError("显式换源目标必须携带在线载荷")
if (
candidate.trusted_source_type.value != candidate.payload_source_type.value
or candidate.trusted_source_key != candidate.payload_source_key
):
raise PluginIdentityConflictError(
"显式换源目标的 trusted 与 payload 来源必须一致"
)
if (
current.trusted_source_type is candidate.trusted_source_type
and current.trusted_source_key == candidate.trusted_source_key
):
raise PluginIdentityConflictError("显式换源的实际来源必须变化")
def _validate_online_binding(
current: PluginIdentity,
candidate: PluginIdentity,
) -> None:
"""校验未绑定身份首次建立在线可信来源的转换边界。"""
if (
current.trusted_source_type is not TrustedPluginSourceType.UNKNOWN
or current.binding_basis not in {
PluginBindingBasis.LEGACY_UNBOUND,
PluginBindingBasis.LOCAL_ONLY,
}
):
raise PluginIdentityConflictError(
"在线绑定只允许当前未绑定的存量或本地身份"
)
if candidate.binding_basis not in {
PluginBindingBasis.OFFICIAL_DEFAULT,
PluginBindingBasis.TOFU,
PluginBindingBasis.EXPLICIT_INSTALL,
}:
raise PluginIdentityConflictError(
"在线绑定目标必须说明官方、TOFU 或显式安装依据"
)
if candidate.trusted_source_type is TrustedPluginSourceType.UNKNOWN:
raise PluginIdentityConflictError("在线绑定目标必须携带可信来源")
if candidate.payload_source_type is PluginPayloadSourceType.UNKNOWN:
if (
current.binding_basis is not PluginBindingBasis.LEGACY_UNBOUND
or candidate.binding_basis not in {
PluginBindingBasis.OFFICIAL_DEFAULT,
PluginBindingBasis.TOFU,
}
):
raise PluginIdentityConflictError(
"仅存量未知来源身份可在不声明载荷来源时建立默认在线绑定"
)
return
if candidate.payload_source_type not in {
PluginPayloadSourceType.OFFICIAL,
PluginPayloadSourceType.THIRD_PARTY,
}:
raise PluginIdentityConflictError("在线绑定目标必须携带在线载荷")
if (
candidate.trusted_source_type.value != candidate.payload_source_type.value
or candidate.trusted_source_key != candidate.payload_source_key
):
raise PluginIdentityConflictError(
"在线绑定目标的 trusted 与 payload 来源必须一致"
)
def _validate_local_binding(
current: PluginIdentity,
candidate: PluginIdentity,
) -> None:
"""校验存量未绑定身份到本地身份的唯一转换方向。"""
if (
current.trusted_source_type is not TrustedPluginSourceType.UNKNOWN
or current.binding_basis is not PluginBindingBasis.LEGACY_UNBOUND
):
raise PluginIdentityConflictError(
"本地绑定只允许当前 unknown + legacy_unbound 身份"
)
if (
candidate.trusted_source_type is not TrustedPluginSourceType.UNKNOWN
or candidate.binding_basis is not PluginBindingBasis.LOCAL_ONLY
or candidate.payload_source_type is not PluginPayloadSourceType.LOCAL
):
raise PluginIdentityConflictError(
"本地绑定目标必须是 unknown + local_only 且携带本地载荷"
)
def _stage_identity_transition(
repository: PluginIdentityRepository,
unit_of_work: PluginIdentityUnitOfWork,
candidate: PluginIdentity,
*,
expected_revision: int,
) -> None:
"""按数据库 revision 条件暂存转换并提交事务。"""
if not repository.stage_replace(
candidate,
expected_revision=expected_revision,
):
raise PluginIdentityConflictError(
f"插件 {candidate.plugin_id} 的来源身份 revision 已被其他任务更新"
)
unit_of_work.commit()