- 迁移 17:plugin_execution(五态、退出码、时长、stdout 摘要、错误留痕) - 沙箱执行器:独立子进程(代码经 stdin JSON 安全传输,不进命令行)、CPU rlimit + RLIMIT_AS(平台不支持时尽力而为并注明)、墙钟超时强杀、输出限额;runner 仅暴露 input/output 作用域 - 执行端点:isolated_process 形态、已安装且启用后可执行;**未启用沙箱一律 503 fail-closed**(HUNJING_SANDBOX_ENABLED 显式开启,审计记录拒绝原因)——AGENTS 第 6 节"无符合策略的 Sandbox 时拒绝执行"落地 - 每次执行落 plugin_execution + 审计(状态/时长);进程级隔离的边界如实注明(网络/文件系统最小化在 Linux 容器档交付) - 测试 96 项全绿(+2):echo 真实执行回填、死循环 5s 超时强杀、异常 failed 含 stderr、无沙箱 503
171 lines
5.1 KiB
Python
171 lines
5.1 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),
|
||
)
|
||
|
||
|
||
@inst_router.post("/{installation_id}/executions", status_code=201)
|
||
async def execute_plugin(
|
||
installation_id: str,
|
||
db: DbSession,
|
||
user: Annotated[User, Depends(require_state_change)],
|
||
request: Request,
|
||
body: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""沙箱执行 isolated 插件:未启用沙箱一律 503(fail-closed)。"""
|
||
domain = await _domain_of(db, user)
|
||
from app.core.config import get_settings
|
||
|
||
execution, result = await service.execute_isolated(
|
||
db, domain.id, installation_id, body.get("input", {}),
|
||
sandbox_enabled=get_settings().sandbox_enabled,
|
||
trace_id=getattr(request.state, "trace_id", None),
|
||
)
|
||
return {
|
||
"execution_id": execution.id, "status": result["status"],
|
||
"duration_ms": result.get("duration_ms"),
|
||
"output": result.get("output"), "error": result.get("error"),
|
||
}
|