Files
hunjing/backend/app/api/v1/deps.py
T
chenxin 684261f4b4 M5:百工与司命——能力注册表、授契行契请命、禁域裁决与观察标证
- 百工域 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 个预存环境失败不变)
2026-08-27 19:17:21 +08:00

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,
)