- 迁移 14:plugin(kind 四形态、撤架三态)、plugin_version(Manifest JSONB + 规范化内容哈希签名、(plugin,version) 唯一)、plugin_installation(enabled + granted_permissions、(domain,plugin) 唯一) - Registry:发布版本(Manifest 必须声明 permissions,签名=canonical 哈希);撤架阻断新调用与新安装,历史审计保留(验证:撤架后 invoked 审计仍在) - 三态分离(购买≠安装≠启用≠授权):安装默认 disabled+零授权;启用必须显式授予权限子集(越界 422);调用门逐级校验(未装/未启/撤架各 403) - 升级:Manifest 新增权限必须重新确认(未确认 422 且明示新增项);确认后升级成功 - 调用:按 Manifest response_template 真实执行(声明式确定性回填);remote/isolated 形态明示 501 随沙箱交付(WP-14),不静默降级;审计含版本哈希(快照语义) - 权限标识统一 group:item 格式 - 测试 86 项全绿(+4):三态流、越界、升级确认、撤架阻断与历史保留
147 lines
4.2 KiB
Python
147 lines
4.2 KiB
Python
"""插件域路由(WP-07)。"""
|
|
|
|
from typing import Annotated, Any
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from sqlalchemy import select
|
|
|
|
from app.auth.dependencies import get_current_user, require_state_change
|
|
from app.conversations.routes import _domain_of
|
|
from app.db.session import DbSession
|
|
from app.models import Plugin, PluginVersion, User
|
|
from app.plugins import service
|
|
|
|
router = APIRouter(prefix="/v1/plugins", tags=["plugins"])
|
|
|
|
|
|
def _plugin_out(p: Plugin) -> dict[str, Any]:
|
|
return {
|
|
"name": p.name, "kind": p.kind, "status": p.status,
|
|
"publisher_domain_id": p.publisher_domain_id,
|
|
}
|
|
|
|
|
|
def _version_out(v: PluginVersion) -> dict[str, Any]:
|
|
return {
|
|
"id": v.id, "plugin_name": v.plugin_name, "version": v.version,
|
|
"manifest": v.manifest, "content_hash": v.content_hash, "status": v.status,
|
|
}
|
|
|
|
|
|
def _installation_out(i: Any) -> dict[str, Any]:
|
|
return {
|
|
"id": i.id, "plugin_name": i.plugin_name, "version_id": i.version_id,
|
|
"enabled": i.enabled, "granted_permissions": i.granted_permissions,
|
|
}
|
|
|
|
|
|
@router.post("/versions", status_code=201)
|
|
async def publish(
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
body: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
_, pv = await service.publish_version(
|
|
db, domain.id, body, getattr(request.state, "trace_id", None)
|
|
)
|
|
return _version_out(pv)
|
|
|
|
|
|
@router.get("")
|
|
async def list_plugins(
|
|
db: DbSession, user: Annotated[User, Depends(get_current_user)]
|
|
) -> dict[str, Any]:
|
|
rows = list(
|
|
(
|
|
await db.scalars(
|
|
select(Plugin).where(Plugin.status == "published").limit(50)
|
|
)
|
|
).all()
|
|
)
|
|
return {"data": [_plugin_out(p) for p in rows]}
|
|
|
|
|
|
@router.post("/{name}/withdrawals")
|
|
async def withdraw(
|
|
name: str,
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
plugin = await service.withdraw(
|
|
db, domain.id, name, getattr(request.state, "trace_id", None)
|
|
)
|
|
return _plugin_out(plugin)
|
|
|
|
|
|
@router.post("/{name}/installations", status_code=201)
|
|
async def install(
|
|
name: str,
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
body: dict[str, str],
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
installation = await service.install(
|
|
db, domain.id, name, body.get("version", ""),
|
|
getattr(request.state, "trace_id", None),
|
|
)
|
|
return _installation_out(installation)
|
|
|
|
|
|
inst_router = APIRouter(prefix="/v1/plugin-installations", tags=["plugins"])
|
|
|
|
|
|
@inst_router.post("/{installation_id}/enablings", status_code=201)
|
|
async def enable(
|
|
installation_id: str,
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
body: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
installation = await service.enable(
|
|
db, domain.id, installation_id, body.get("granted_permissions", []),
|
|
getattr(request.state, "trace_id", None),
|
|
)
|
|
return _installation_out(installation)
|
|
|
|
|
|
@inst_router.post("/{installation_id}/upgrades", status_code=201)
|
|
async def upgrade(
|
|
installation_id: str,
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
body: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
installation = await service.upgrade(
|
|
db, domain.id, installation_id, body.get("version", ""),
|
|
body.get("granted_permissions"), getattr(request.state, "trace_id", None),
|
|
)
|
|
return _installation_out(installation)
|
|
|
|
|
|
invoke_router = APIRouter(prefix="/v1/plugins", tags=["plugins"])
|
|
|
|
|
|
@invoke_router.post("/{name}/invocations")
|
|
async def invoke(
|
|
name: str,
|
|
db: DbSession,
|
|
user: Annotated[User, Depends(require_state_change)],
|
|
request: Request,
|
|
body: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
domain = await _domain_of(db, user)
|
|
return await service.invoke(
|
|
db, domain.id, name, body.get("input", {}), body.get("run_id"),
|
|
getattr(request.state, "trace_id", None),
|
|
)
|