✨ feat: Sandbox Runtime——isolated 插件子进程隔离执行(WP-06/07 收口)
- 迁移 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
This commit is contained in:
1 parent
21eacc8c75
commit
99a7789432
8 files changed
+389
No files matched your search
@@ -297,3 +297,70 @@ async def invoke(
|
||||
)
|
||||
await db.flush()
|
||||
return output
|
||||
|
||||
|
||||
async def execute_isolated(
|
||||
db: AsyncSession,
|
||||
domain_id: str,
|
||||
installation_id: str,
|
||||
input_: dict[str, Any],
|
||||
sandbox_enabled: bool,
|
||||
trace_id: str | None,
|
||||
) -> tuple[Any, dict[str, Any]]:
|
||||
"""隔离执行:isolated 形态插件经沙箱子进程运行;未启用沙箱一律拒绝。
|
||||
|
||||
返回(执行记录, 结果)。代码体取自 Manifest 的 code 字段
|
||||
(部署者明确信任的本地代码插件,仍受资源限制约束)。
|
||||
"""
|
||||
from app.models import PluginExecution
|
||||
from app.sandbox.executor import run_sandboxed
|
||||
|
||||
installation = await require_installation(db, domain_id, installation_id)
|
||||
plugin = await db.get(Plugin, installation.plugin_name)
|
||||
assert plugin is not None
|
||||
if plugin.kind != "isolated_process":
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY, "仅 isolated_process 形态走沙箱执行"
|
||||
)
|
||||
if not installation.enabled:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "插件未启用")
|
||||
if not sandbox_enabled:
|
||||
db.add(
|
||||
AuditEvent(
|
||||
actor_type="user", actor_id=domain_id, action="plugin.execution_denied",
|
||||
aggregate_type="plugin", aggregate_id=plugin.name,
|
||||
payload={"reason": "sandbox_not_enabled"}, trace_id=trace_id,
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
raise HTTPException(
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
"沙箱未启用:拒绝执行(fail-closed),启用请设置 HUNJING_SANDBOX_ENABLED",
|
||||
)
|
||||
pv = await db.get(PluginVersion, installation.version_id)
|
||||
assert pv is not None
|
||||
code = str(pv.manifest.get("code", ""))
|
||||
execution = PluginExecution(installation_id=installation.id, status="running")
|
||||
db.add(execution)
|
||||
await db.flush()
|
||||
result = await run_sandboxed(code, input_)
|
||||
execution.status = result["status"]
|
||||
execution.exit_code = result.get("exit_code")
|
||||
execution.duration_ms = result.get("duration_ms")
|
||||
execution.stdout_excerpt = result.get("stdout_excerpt")
|
||||
execution.error = result.get("error")
|
||||
from datetime import UTC
|
||||
from datetime import datetime as _dt
|
||||
|
||||
execution.completed_at = _dt.now(UTC)
|
||||
db.add(
|
||||
AuditEvent(
|
||||
actor_type="user", actor_id=domain_id, action="plugin.executed",
|
||||
aggregate_type="plugin", aggregate_id=plugin.name,
|
||||
payload={"execution_id": execution.id, "status": execution.status,
|
||||
"duration_ms": execution.duration_ms},
|
||||
trace_id=trace_id,
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
return execution, result
|
||||
Reference in new issue
Block a user