- 百工域 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 个预存环境失败不变)
166 lines
5.9 KiB
Python
166 lines
5.9 KiB
Python
"""API 层依赖注入。
|
|
|
|
从 ``app.state`` 取 lifespan 装配的共享运行时(乾坤沙箱客户端 / EventStore)。
|
|
CRUD 类路由直接调用 ``services`` 函数(函数内部自管 ``AsyncSessionLocal``),不在此注入 session。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
|
|
from fastapi import Depends, HTTPException, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.config import settings
|
|
from app.db.session import get_session
|
|
from app.integrations.sandbox.client import SandboxClient
|
|
from app.modules.runtime.events import EventStore
|
|
from app.schemas.auth import CurrentUser as CurrentUserSchema
|
|
from app.services.auth import decode_token, get_user_by_id, get_user_permissions
|
|
|
|
|
|
def _unavailable(message: str) -> HTTPException:
|
|
return HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "service_unavailable", "message": message}},
|
|
)
|
|
|
|
|
|
def get_sandbox_client(request: Request) -> SandboxClient:
|
|
sandbox: SandboxClient | None = getattr(request.app.state, "baigong_sandbox", None)
|
|
if sandbox is None:
|
|
raise _unavailable("乾坤沙箱未就绪(SANDBOX 未配置或未启动)")
|
|
return sandbox
|
|
|
|
|
|
def get_event_store(request: Request) -> EventStore:
|
|
store = getattr(request.app.state, "event_store", None)
|
|
if store is None:
|
|
raise _unavailable("事件存储未就绪")
|
|
return store
|
|
|
|
|
|
async def require_admin(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> None:
|
|
"""管理端点鉴权:JWT + is_admin 优先,ADMIN_TOKEN fallback。
|
|
|
|
JWT 模式下:
|
|
- 有效 JWT 且 is_admin=True → 放行
|
|
- 有效 JWT 且 is_admin=False → 403
|
|
- 无效 JWT → 回退 ADMIN_TOKEN 校验
|
|
ADMIN_TOKEN 模式下(原 v1 逻辑):
|
|
- 同现有逻辑,常数时间比较
|
|
- ADMIN_TOKEN 未配置且无 JWT → 503 fail-closed
|
|
"""
|
|
# 0) fail-closed:两种鉴权均未配置时直接拒绝
|
|
if not settings.jwt_secret and not settings.admin_token:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}},
|
|
)
|
|
|
|
credential = request.headers.get("Authorization", "")
|
|
if not credential.startswith("Bearer "):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "缺少管理员凭证。"}},
|
|
)
|
|
presented = credential[len("Bearer ") :]
|
|
|
|
# 1) 尝试 JWT 模式(如果 jwt_secret 已配置)
|
|
if settings.jwt_secret:
|
|
try:
|
|
payload = decode_token(presented)
|
|
if payload.get("type") == "access":
|
|
user_id = payload.get("sub")
|
|
user = await get_user_by_id(db, user_id)
|
|
if user is None or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}
|
|
},
|
|
)
|
|
if not user.is_admin:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": {"code": "forbidden", "message": "仅管理员可访问此接口。"}
|
|
},
|
|
)
|
|
return # JWT 模式鉴权通过
|
|
except HTTPException:
|
|
raise
|
|
except Exception:
|
|
pass # JWT 无效,回退 ADMIN_TOKEN 校验
|
|
|
|
# 2) Fallback: ADMIN_TOKEN 校验(原 v1 逻辑)
|
|
token = settings.admin_token
|
|
if not token:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}},
|
|
)
|
|
if not hmac.compare_digest(presented, token):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "管理员凭证无效。"}},
|
|
)
|
|
|
|
|
|
async def get_current_user(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> CurrentUserSchema:
|
|
"""从 Authorization: Bearer <access_token> 解析当前用户。
|
|
|
|
JWT 未配置(jwt_secret 为空)时 503 fail-closed。
|
|
"""
|
|
if not settings.jwt_secret:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail={
|
|
"error": {
|
|
"code": "service_unavailable",
|
|
"message": "认证服务未配置(JWT_SECRET 为空)。",
|
|
}
|
|
},
|
|
)
|
|
credential = request.headers.get("Authorization", "")
|
|
if not credential.startswith("Bearer "):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "缺少认证凭证。"}},
|
|
)
|
|
token = credential[len("Bearer ") :]
|
|
try:
|
|
payload = decode_token(token)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "Token 无效或已过期。"}},
|
|
) from None
|
|
if payload.get("type") != "access":
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "Token 类型不匹配。"}},
|
|
)
|
|
user_id = payload.get("sub")
|
|
user = await get_user_by_id(db, user_id)
|
|
if user is None or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}},
|
|
)
|
|
perms = await get_user_permissions(db, user.id)
|
|
return CurrentUserSchema(
|
|
user_id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
owner_scope=user.id,
|
|
is_admin=user.is_admin,
|
|
permissions=perms,
|
|
)
|