- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明) - 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301 - 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账 - MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch) - 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证) - 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页 - 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
116 lines
4.9 KiB
Python
116 lines
4.9 KiB
Python
"""百工执行器:按能力执行类型分派(builtin / qiankun 乾坤 / mcp / http)。
|
|
|
|
乾坤经 ``integrations/sandbox`` 的 SandboxClient——HMAC 短期凭证授契由沙箱客户端
|
|
签发(SANDBOX_AUTH_SECRET 两端一致),执行审计证据经行动账落库。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shlex
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from sqlalchemy import select
|
|
|
|
from app.core.errors import AppError
|
|
from app.db.session import AsyncSessionLocal
|
|
from app.domains.baigong.models import Capability
|
|
from app.integrations.mcp.client import call_tool
|
|
from app.integrations.sandbox.client import SandboxClient
|
|
from app.integrations.sandbox.models import ExecRequest as SandboxExecRequest
|
|
|
|
_HTTP_TIMEOUT_SECONDS = 30.0
|
|
|
|
|
|
@dataclass
|
|
class ExecOutcome:
|
|
ok: bool
|
|
output: str
|
|
error_code: str | None = None
|
|
duration_ms: int = 0
|
|
|
|
|
|
class Executor:
|
|
"""能力执行分派器。构造时注入乾坤客户端(未配置则乾坤类能力 503)。"""
|
|
|
|
def __init__(self, sandbox: SandboxClient | None, http: httpx.AsyncClient | None = None) -> None:
|
|
self._sandbox = sandbox
|
|
self._http = http
|
|
|
|
async def execute(self, capability: Capability, params: dict[str, Any]) -> ExecOutcome:
|
|
dispatch = {
|
|
"builtin": self._exec_builtin,
|
|
"qiankun": self._exec_qiankun,
|
|
"mcp": self._exec_mcp,
|
|
"http": self._exec_http,
|
|
}
|
|
handler = dispatch.get(capability.executor_type)
|
|
if handler is None:
|
|
raise AppError("invalid_request", f"未知执行类型:{capability.executor_type}")
|
|
return await handler(capability, params)
|
|
|
|
# ── 内置:无副作用纯计算(时间/回显等) ──
|
|
async def _exec_builtin(self, capability: Capability, params: dict[str, Any]) -> ExecOutcome:
|
|
from app.core.time import to_iso, utc_now
|
|
|
|
if capability.executor_ref == "echo":
|
|
return ExecOutcome(ok=True, output=f"[echo] {params.get('text', '')}")
|
|
if capability.executor_ref == "datetime.now":
|
|
return ExecOutcome(ok=True, output=to_iso(utc_now()))
|
|
raise AppError("not_found", f"内置能力不存在:{capability.executor_ref}")
|
|
|
|
# ── 乾坤:不可信执行隔离(HMAC 授契由 SandboxClient 签发) ──
|
|
async def _exec_qiankun(self, capability: Capability, params: dict[str, Any]) -> ExecOutcome:
|
|
if self._sandbox is None:
|
|
raise AppError(
|
|
"service_unavailable", "乾坤沙箱未就绪(SANDBOX 未配置),无法执行。", status_code=503
|
|
)
|
|
command = str(params.get("command", ""))
|
|
if not command:
|
|
raise AppError("invalid_request", "qiankun 能力需要 command 参数")
|
|
result = await self._sandbox.exec(SandboxExecRequest(argv=shlex.split(command)))
|
|
output = result.stdout or result.stderr or ""
|
|
return ExecOutcome(
|
|
ok=result.ok,
|
|
output=output,
|
|
error_code=None if result.ok else "qiankun_exit",
|
|
duration_ms=result.duration_ms,
|
|
)
|
|
|
|
# ── MCP:外部能力服务(博闻等);executor_ref = mcp_servers.name ──
|
|
async def _exec_mcp(self, capability: Capability, params: dict[str, Any]) -> ExecOutcome:
|
|
from app.models.mcp_servers import McpServer
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
server = (
|
|
await session.execute(
|
|
select(McpServer).where(McpServer.name == capability.executor_ref)
|
|
)
|
|
).scalars().first()
|
|
if server is None or not server.endpoint_url:
|
|
raise AppError(
|
|
"not_found", f"MCP 服务未登记或无端点:{capability.executor_ref}"
|
|
)
|
|
endpoint, token = server.endpoint_url, server.auth_token
|
|
session.expunge(server)
|
|
|
|
client = self._http or httpx.AsyncClient(trust_env=False, timeout=_HTTP_TIMEOUT_SECONDS)
|
|
result = await call_tool(client, endpoint, capability.name.removeprefix(f"mcp_{capability.executor_ref}_"), params, token)
|
|
payload = getattr(result, "content", None) or result
|
|
return ExecOutcome(ok=True, output=json.dumps(payload, ensure_ascii=False, default=str))
|
|
|
|
# ── HTTP:受授契约束的外部 HTTP 动作 ──
|
|
async def _exec_http(self, capability: Capability, params: dict[str, Any]) -> ExecOutcome:
|
|
url = str(params.get("url", ""))
|
|
if not url:
|
|
raise AppError("invalid_request", "http 能力需要 url 参数")
|
|
async with httpx.AsyncClient(trust_env=False, timeout=_HTTP_TIMEOUT_SECONDS) as client:
|
|
resp = await client.request(str(params.get("method", "GET")).upper(), url)
|
|
return ExecOutcome(
|
|
ok=resp.status_code < 400,
|
|
output=f"HTTP {resp.status_code}: {resp.text[:500]}",
|
|
error_code=None if resp.status_code < 400 else f"http_{resp.status_code}",
|
|
)
|