fix(deps): upgrade OpenAI stack and resolve dependency vulnerabilities

This commit is contained in:
chenmingxuan committed 2026-10-28 11:22:04 +08:00
1 parent bb873ec0e0
commit 6b2c0f199d
14 files changed
+2737 -2731

No files matched your search

+1856 -2311
View File
File diff suppressed because it is too large. Load diff
+16 -11
View File
@@ -9,24 +9,29 @@
"test": "ng test" "test": "ng test"
}, },
"private": true, "private": true,
"overrides": {
"@angular/build": {
"vite": "8.3.5"
}
},
"dependencies": { "dependencies": {
"@angular/cdk": "^22.1.5", "@angular/cdk": "^22.2.4",
"@angular/common": "^22.1.4", "@angular/common": "^22.2.3",
"@angular/compiler": "^22.1.4", "@angular/compiler": "^22.2.3",
"@angular/core": "^22.1.4", "@angular/core": "^22.2.3",
"@angular/forms": "^22.1.4", "@angular/forms": "^22.2.3",
"@angular/platform-browser": "^22.1.4", "@angular/platform-browser": "^22.2.3",
"@angular/platform-browser-dynamic": "^22.1.4", "@angular/platform-browser-dynamic": "^22.2.3",
"@angular/router": "^22.1.4", "@angular/router": "^22.2.3",
"marked": "18.1.1", "marked": "18.1.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^22.1.6", "@angular/build": "^22.2.4",
"@angular/cli": "^22.1.6", "@angular/cli": "^22.2.4",
"@angular/compiler-cli": "^22.1.4", "@angular/compiler-cli": "^22.2.3",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"istanbul-lib-instrument": "^6.0.3", "istanbul-lib-instrument": "^6.0.3",
"jasmine-core": "~5.6.0", "jasmine-core": "~5.6.0",
+6
View File
@@ -30,6 +30,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum from enum import StrEnum
import random import random
import re
from functools import lru_cache from functools import lru_cache
import threading import threading
import time import time
@@ -124,6 +125,11 @@ def classify_failure(error: BaseException) -> Failure:
# Programmer/request errors: retrying is useless and falling back only # Programmer/request errors: retrying is useless and falling back only
# hides the bug inside a different model's output. # hides the bug inside a different model's output.
return Failure(FailureCategory.BAD_REQUEST, False, False) return Failure(FailureCategory.BAD_REQUEST, False, False)
# langchain-openai 1.7 raises this ValueError for a response.failed event
# without provider error details, before our completion guard sees it.
# Preserve the bounded invalid-output retry policy for these failed streams.
if isinstance(error, ValueError) and re.fullmatch(r"Response \S+ failed\.", str(error)):
return Failure(FailureCategory.INVALID_OUTPUT, True, True)
return Failure(FailureCategory.UNKNOWN, True, True) return Failure(FailureCategory.UNKNOWN, True, True)
+4
View File
@@ -1,5 +1,9 @@
# 开发交接:2026-10-19 # 开发交接:2026-10-19
2026-10-28 配套升级 OpenAI/LangChain 与安全依赖,审计范围、版本、验证及容器构建限制见[依赖安全更新记录](plans/2026-10-28-dependency-security.md)。
2026-10-28 会话已接续至 **ARTEMIS 2.3.0**,最新提交、插件发布状态、产品约定和待验证范围见[本次交接记录](plans/2026-10-28-session-handoff.md)及[2.3.0 发行说明](releases/2.3.0.md)。以下各日期内容保留为历史快照,不代表当前待办。
2026-10-27 发布主线已整理为 **ARTEMIS 2.2.0**,配套插件为 **Cua 0.38.0+artemis.1**,Helper 为 **1.2.3**。最新升级内容、验证与未测范围以[2.2.0 发行说明](releases/2.2.0.md)为准;下方“尚未发布”、分叉计数和候选版本均属于对应日期快照。插件从独立工作树发布,原插件主目录的 OCU 开发现场保留。 2026-10-27 发布主线已整理为 **ARTEMIS 2.2.0**,配套插件为 **Cua 0.38.0+artemis.1**,Helper 为 **1.2.3**。最新升级内容、验证与未测范围以[2.2.0 发行说明](releases/2.2.0.md)为准;下方“尚未发布”、分叉计数和候选版本均属于对应日期快照。插件从独立工作树发布,原插件主目录的 OCU 开发现场保留。
2026-10-26 已再次接续开发会话,当前基线为主程序本地提交 `fac4ccb`(2.2.0)。两个仓库现场、已撤回机制与后续验收范围见[本次接续记录](plans/2026-10-26-session-handoff.md)。以下继续保留原始交接快照。 2026-10-26 已再次接续开发会话,当前基线为主程序本地提交 `fac4ccb`(2.2.0)。两个仓库现场、已撤回机制与后续验收范围见[本次接续记录](plans/2026-10-26-session-handoff.md)。以下继续保留原始交接快照。
@@ -0,0 +1,87 @@
# 2026-10-28 依赖安全更新与兼容性验证
## 审计结果
范围为主仓库声明的 Python、前端 npm、后台原型依赖及可识别的构建依赖。使用当日 PyPI/pip-audit、npm audit、OSV 和上游安全公告;没有忽略漏洞或降低检查级别。
| 范围 | 更新前 | 更新后 |
| --- | --- | --- |
| `uv.lock` 中全部 169 个 registry 包,含开发依赖及非 Windows 平台包 | 11 个包命中,97 条公告记录 | 0 条,未跳过包 |
| 前端 npm 锁文件 | 17 个受影响包:3 critical、11 high、3 moderate | 0 个受影响包 |
| `playground/backend_manager/requirements.txt` 当日解析结果 | 54 个包,2 个包命中、3 条记录 | 52 个包,0 条,未跳过包 |
| Python 构建工具的当日解析结果 | — | setuptools 84.2.1、Cython 3.3.0,0 条 |
| Android Gradle Plugin 8.5.0 | — | OSV 直接包查询未检出公告;未解析完整 Gradle 构建工具传递依赖 |
公告可能含重复 ID 或别名,97 条不代表 97 个独立 CVE;npm 受影响包数也包含依赖链上的父包。零结果仅表示本次数据库未检出已知漏洞,不代表没有未知风险或业务安全问题。
远程 Python SDK 不含第三方运行时依赖;Android Helper 使用系统 API,不含第三方运行时库。独立 `Artemis-Plugins` 仓库、用户安装的插件、系统软件包和完整容器镜像不在上述零漏洞结论内。
## 版本调整
| 包 | 原版本 | 更新后 |
| --- | --- | --- |
| openai | 3.3.0 | 3.40.0,沿用同日 SDK 更新 |
| langchain / langchain-core | 1.3.15 / 1.5.6 | 1.4.7 / 1.7.0 |
| langchain-openai | 1.5.2 | 1.7.3 |
| langgraph / langgraph-sdk | 1.2.11 / 0.4.2 | 1.2.18 / 0.4.10 |
| httpx | 0.28.1 | 保持 0.28.1,当日最新版 |
| httpx2 / httpcore2 | 2.12.0 | 2.16.0 |
| mcp | 1.29.0 | 1.30.0 |
| aiohttp | 3.14.3 | 3.14.5 |
| certifi | 2026.7.22 | 2026.10.23 |
| filelock | 3.32.3 | 4.2.4 |
| multidict | 6.7.1 | 7.2.1 |
| pillow | 12.3.0 | 13.0.0 |
| pyjwt | 2.13.0 | 2.15.1 |
| tornado | 6.5.8 | 6.6 |
| urllib3 | 2.7.0 | 2.8.0 |
| virtualenv | 21.7.4 | 21.15.1 |
| Angular framework / compiler-cli | 22.1.4 | 22.2.3 |
| Angular CLI / build | 22.1.6 | 22.2.4 |
| Angular CDK | 22.1.5 | 22.2.4 |
| Vite | 8.1.5 | 8.3.5 |
| Piscina | 5.2.0 | 5.3.2 |
| 原型 Nginx 基础镜像 | 1.27-alpine | 1.30.6-alpine |
`langchain-community 0.4.2` 和 `langchain-mcp-adapters 0.3.2` 保持不变。后者要求 MCP `<2`,因此主项目声明 `mcp>=1.30.0,<2.0.0`,使用兼容的 1.x 安全修复版本。其余联动升级见 `uv.lock`。
Angular build 22.2.4 仍精确依赖存在漏洞的 Vite 8.3.0,通过限定在 `@angular/build` 下的 npm override 使用 8.3.5,未使用 `--force` 或 `--legacy-peer-deps`。以后上游采用安全版本时可移除 override,重新审计并验证构建。
后台原型的 `python-jose 3.5.0` 和间接 `ecdsa 0.19.2` 没有可用修复版,改为 `PyJWT[crypto]>=2.15.1,<3.0.0`,保持算法白名单与 HTTP 401 错误处理。原型仍按 requirements 动态解析;该扫描结果不是所有旧镜像或旧安装环境的审计证明。
## 必要的适配
- `langchain-openai 1.7.x` 对没有错误详情的 `response.failed` 抛出 `ValueError("Response <id> failed.")`。补充精确分类,保留 `INVALID_OUTPUT` 的有限重试;未把所有 ValueError 当成无效模型输出。
- 补充失败流重试回归,验证不完整输出不会成为正式结论或工具动作,重试保留已有工具证据。
- Responses 模拟传输改用 OpenAI 默认使用的 httpx2,并修正测试夹具中 `request` 同名遮蔽,确保超过 1 MiB 的中文工具参数确实经过分块 SSE/UTF-8 传输。
- 原型 JWT 回归覆盖签发、声明、当前用户、过期、错误签名、算法不匹配、无签名、畸形 token 和缺失 subject。
- MCP 的 audience 漏洞需要服务端配置配合,单纯升级不自动开启校验。本仓库没有配置 `AuthSettings`/`token_verifier`,当前不使用该 bearer-auth 路径;未来接入时必须同时设置 `validate_token_resource` 并提供正确的 token resource。
## 验证
- 隔离环境完整默认 Python 测试:3207 通过、6 跳过、8 排除;102 条现有 Pillow `getdata()` 弃用警告,该接口计划在 Pillow 14 移除。
- 随后改用 httpx2 的模型流、失败分类和 JWT 专项测试:41 通过;同步原 `.venv` 后同一组 41 项再次通过。
- Ruff 格式/静态检查、质量基线、核心 Pyright 类型检查通过;Pyright 额外显式使用了升级后的解释器。
- 锁文件校验、升级环境 `uv pip check`、`git diff --check` 通过。
- 前端 ChromeHeadless:336 通过;生产构建通过,生成产物不纳入提交。
- 主 Python、原型 Python、Python 构建工具及前端更新后复扫无已知漏洞。
- Nginx 1.30.6 为当日官方稳定修复版本,Docker Hub 标签存在且 active。已尝试构建原型镜像,但 Docker Hub registry/auth 连接超时,尚未完成容器构建和运行验证,也未扫描完整镜像内的 OS 包。
- 未调用付费模型、执行设备业务测试、发布提交或修改用户任务配置。
原始审计 JSON、解析清单和测试日志保存在本地被 Git 忽略的 `artifacts/dependency-audit-20261028/`。
## 生效
初次同步受到运行中服务占用 Pillow DLL 的阻止,已恢复旧环境并通过依赖一致性检查。用户随后明确表示已停止服务,原 `.venv` 已成功执行 `uv sync --dev --locked` 并再次通过版本核对与兼容性测试。
随后通过 `artemis restart --no-open` 恢复后台服务;`/api/system/server-status` 返回 running,首页 HTTP 200 且引用本次新前端构建。原环境全部 165 个已安装 distribution 的版本均与锁文件匹配。
本次沿用 ARTEMIS 2.3.0,按用户要求提交源码和锁文件,不发布。用户已明确无需构建 Docker,本次不再继续容器构建;修改 Dockerfile 不会更新已有容器。
## 上游依据
- [Pillow 13.0.0 安全修复](https://pillow.readthedocs.io/en/stable/releasenotes/13.0.0.html)
- [MCP bearer token audience 公告](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-w4fh-qvv9-3v23)
- [Vite/launch-editor Windows 命令执行公告](https://github.com/advisories/GHSA-587h-7wmj-h5pm)
- [Vite 8.3.5 发布说明](https://github.com/vitejs/vite/releases/tag/v8.3.5)
- [Nginx 安全公告](https://nginx.org/en/security_advisories.html)及[当前稳定版本](https://nginx.org/en/download.html)
+35
View File
@@ -0,0 +1,35 @@
# OpenAI Python SDK 3.40.0 适配
本文保留 SDK 单独升级阶段的结果。同日后续的 LangChain、HTTPX2 与安全依赖升级,以及最终验证与生效状态,见[依赖安全更新记录](2026-10-28-dependency-security.md)。
## 范围
- `pyproject.toml` 显式声明 `openai>=3.40.0,<4.0.0`;项目直接使用 SDK 的 HTTP 客户端,不再仅依赖 `langchain-openai` 间接带入。
- `uv.lock` 将 SDK 从 3.3.0 升至 3.40.0,保留既有 `langchain-openai 1.5.2`、`httpx2 2.12.0` 及其他包版本。锁文件记录发行包和 SHA-256。
- 继续使用 Responses API、`store=False` 与 ARTEMIS 的重试预算;SDK `max_retries=0`,不叠加 SDK 重试。
- 没有修改模型默认值、已有用户配置、设备调度或插件协议,主程序版本仍为 2.3.0。
## 兼容性核对
从 3.3.0 到 3.40.0 的更新涉及 SSE 解码、异步流关闭、传输异常归一化、重试验证、Responses 类型及缓存元数据。现有 Responses 调用与已锁定的 LangChain 适配层在专项回归中兼容,无需新增业务兼容分支。
3.40.0 增加显式缓存断点 TTL 类型。此次只升级 SDK,不自动启用显式缓存断点或改变 TTL;以后接入产品配置时,需要同时处理服务端与模型支持、参数入口和用量展示,不能将 SDK 类型支持当作当前端点支持。
新增回归覆盖超过 1 MiB 的中文工具参数,在 SSE 帧和 UTF-8 字符跨网络块时完整还原;即使参数完整,缺少 `response.completed` 仍拒绝输出正式结论。
## 本次验证
- 在 OpenAI SDK 3.40.0 下,模型调用专项回归 97 项通过;完整默认回归 3198 项通过、6 项跳过、8 项排除,保留 102 条已有 Pillow 弃用警告。
- Ruff 格式与静态检查、质量基线、核心 Pyright 类型检查、`uv pip check`、`uv lock --check` 通过。
- 本次未改前端,未运行前端测试或构建;未执行真实模型或设备业务验收。
## 生效与边界
开发环境已通过 `uv sync --dev --locked` 同步。已有长驻服务需正常重启后完整加载新 SDK,本次不主动重启。验证使用本地模拟传输,不调用付费模型或真实设备。
## 来源
- [OpenAI Python SDK 3.40.0 发布说明](https://github.com/openai/openai-python/releases/tag/v3.40.0)
- [SDK 变更记录](https://github.com/openai/openai-python/blob/v3.40.0/CHANGELOG.md)
- [官方 Python SDK 文档](https://developers.openai.com/api/reference/python)
- [官方提示词缓存说明](https://developers.openai.com/api/docs/guides/prompt-caching)
+47
View File
@@ -0,0 +1,47 @@
# ARTEMIS 开发主线交接:2026-10-28
本次依据用户提供的《ARTEMIS开发主线-1027》完整会话记录、仓库发行说明与本地 Git 状态接续开发。agentmemory 未返回匹配本项目目录的会话,因此不采用其他项目的记忆。此次未联网刷新远端、重跑测试、重启服务或操作设备;发布和实测结论来自上一会话记录,不能视为本次重新验证。
## 当前基线
| 范围 | 本次本地核对结果 |
| --- | --- |
| 主程序 | `main` 与本地 `eapil/main` 位于 `bb873ec`,版本 2.3.0;迁入前已跟踪文件无修改,只有未跟踪的 `.angular/` 缓存 |
| 人工评审插件工作树 | 插件仓库 `.tmp/task-review-extension`,分离 HEAD `386f108`,工作树干净;`7bcdb2a` 增加插件,`386f108` 上架市场 |
| Cua 工作树 | 插件仓库 `.tmp/cua-shell-observation`,分支 `codex/cua-shell-observation`,提交 `b780a60`,工作树干净 |
| 插件原主目录 | `main` 位于 `757b829`,相对本地 `origin/main` 领先 4、落后 10;仍有 OCU、Cua 和测试脚本等既有修改与未跟踪文件,必须保留 |
上一会话已完成 ARTEMIS 2.2.0、Cua `0.38.0+artemis.1`、ARTEMIS 2.3.0 和任务人工评审 1.0.0 的推送及发布;Cua 继续标注预览版。最后已核验远端提交、发行附件下载校验值,并完成线上市场隔离安装和启用验证。早期记录中的“2.2.0 尚未发布”“Cua 尚未提交”已经过时。
## 已完成的开发与约定
- 2.2.0 接入仅 PC 测试,网页、MCP、参数 JSON 和 Python SDK 使用 `execution_target="computer"`;PC、安卓和跨端任务统一全局串行。
- 已修复安卓缩放截图的校验坐标、观测恢复预算、输入首次清空与空值确认、Helper 聚焦目标选择、Pro 收尾空转和重复计划、MCP 排队及运行状态、笔记截图路径和验收项展示标签匹配。Helper 升级至 1.2.3。动作卡片文字可复制,长 RESULT 默认折叠。
- 模型选项增加 `gpt-6.1-terra`,新安装探索默认 `gpt-6.1-terra / high`;保存配置优先。
- 2.3.0 提供 `schema_version: 3` 的声明式 `workspace` 插件接口。独立「任务人工评审」插件从市场安装启用,主程序不默认安装启用。
- 回放标题栏控制左右侧栏,普通浮窗和影院模式均支持;单侧展开仍位于对应左右侧,窄屏一次展开一侧。
- 左侧只展示本次运行最初提交的需求提示词,支持 Markdown,不混入模型拆解步骤或后续指导。
- 右侧由人工填写「AI执行准确性」「测试结果正确性」,使用中文「通过 / 失败」,两项及备注均选填,可以修改或清空。结果正确性评价 ARTEMIS 判断是否准确,与业务用例是否通过区分。
- 评审不调用 AI,不覆盖原执行结论或既有人工复验;运行中只读,结束后可编辑,保留修改历史,并防止并发覆盖和重复写入。
- 评审绑定单次运行,复跑不继承;禁用或卸载保留数据但隐藏界面及新导出字段,重新安装启用后恢复,删除任务时清理。
- 已启用插件的字段进入单任务 Word/Excel、批次汇总及资料包报告,空项留空。重新导出反映最新评审,已下载旧文件不变。
- 插件 SDK、工作区接口文档及随程序分发的 Skill 已同步。2.3.0 使用 `catalog-v3.json`,旧 `catalog.json` 保持兼容 2.2.x;执行插件更新需兼顾两个索引,工作区插件只加入新版。
- 已撤回的次数配额、内容指纹和 AI 二次动作否决不恢复;已移除的设备动作回放不恢复,历史步骤和截图回看保留。
## 历史验证与仍待覆盖范围
- 2.2.0 发布前记录:后端 3189 项、前端 326 项、SDK 24 项通过,构建及质量检查通过。集中回归 9/9 正式实测通过;后续对排队状态、截图路径、验收项标签另有修复和实测证据。
- 2.3.0 发布前记录:后端默认回归 3197 项、插件针对性回归 49 项、前端 336 项通过,构建及静态检查通过;Windows 隔离示例验证主题、左右布局、中文评审保存与 Word/Excel 导出。该轮未执行真实设备业务或付费模型测试,macOS/Linux 未实测。
- Cua 真实鼠标跟随问题按用户此前要求暂缓;原任务不足以证明具体误点根因,不能因驱动已发布就标为已解决。
- 真实断线恢复、小时级稳定性、多显示器及多 DPI 等扩大兼容性场景仍缺完整验证。发布完成不等于这些场景全部通过。
- 素材准备句柄、跨次任务取代关系仍只是产品候选,尚未立项。
当前没有尚未完成的发布步骤或待回答的产品确认。本次仅迁移上下文;后续按用户新需求继续,修改前重新核对相关仓库和工作树,不自动恢复旧设备任务或重复发布。
## 参考
- [2.2.0 发行说明](../releases/2.2.0.md)
- [2.3.0 发行说明](../releases/2.3.0.md)
- [工作区插件接口](../workspace-plugins.md)
- [插件开发 SDK](../plugin-development.md)
- [上次会话接续记录](2026-10-26-session-handoff.md)
@@ -15,7 +15,8 @@
from datetime import datetime, timedelta, timezone, UTC from datetime import datetime, timedelta, timezone, UTC
from fastapi import Depends, HTTPException, Security, status from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt import jwt
from jwt import InvalidTokenError
from app.config import settings from app.config import settings
security = HTTPBearer(auto_error=False) security = HTTPBearer(auto_error=False)
@@ -40,7 +41,7 @@ def decode_access_token(token: str) -> dict:
try: try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]) payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
return payload return payload
except JWTError as e: except InvalidTokenError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid or expired credentials: {str(e)}", detail=f"Invalid or expired credentials: {str(e)}",
+1 -1
View File
@@ -2,7 +2,7 @@ fastapi>=0.115.0
uvicorn[standard]>=0.32.0 uvicorn[standard]>=0.32.0
pydantic>=2.9.0 pydantic>=2.9.0
pydantic-settings>=2.5.0 pydantic-settings>=2.5.0
python-jose[cryptography]>=3.3.0 PyJWT[crypto]>=2.15.1,<3.0.0
passlib[bcrypt]>=1.7.4 passlib[bcrypt]>=1.7.4
docker>=7.1.0 docker>=7.1.0
google-cloud-bigquery>=3.26.0 google-cloud-bigquery>=3.26.0
+1 -1
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
FROM nginx:1.27-alpine FROM nginx:1.30.6-alpine
# Install openssl for auto-generating self-signed dev certificates and curl for healthchecks # Install openssl for auto-generating self-signed dev certificates and curl for healthchecks
RUN apk add --no-cache openssl curl bash RUN apk add --no-cache openssl curl bash
+8 -7
View File
@@ -27,14 +27,15 @@ dependencies = [
# Remote Python SDK (kept as a zero-dependency workspace package) # Remote Python SDK (kept as a zero-dependency workspace package)
"artemis-client>=0.1.0", "artemis-client>=0.1.0",
# Orchestration & Framework # Orchestration & Framework
"langgraph>=1.0.2,<2.0.0", "langgraph>=1.2.18,<2.0.0",
"langchain>=1.0.0", "langchain>=1.4.7,<2.0.0",
"langchain-core>=1.0.0", "langchain-core>=1.7.0,<2.0.0",
"langchain-community>=0.4.1", "langchain-community>=0.4.1",
"langchain-mcp-adapters>=0.2.0", "langchain-mcp-adapters>=0.2.0",
"mcp>=1.26.0", "mcp>=1.30.0,<2.0.0",
# LLM Providers & GenAI SDKs # LLM Providers & GenAI SDKs
"langchain-openai>=1.0.0", "langchain-openai>=1.7.3,<2.0.0",
"openai>=3.40.0,<4.0.0",
# Configuration, Models & Validation # Configuration, Models & Validation
"pydantic>=2.7.0", "pydantic>=2.7.0",
"pydantic-settings>=2.10.1", "pydantic-settings>=2.10.1",
@@ -59,7 +60,7 @@ dependencies = [
"websockets>=12.0", "websockets>=12.0",
"sseclient-py>=1.8.0", "sseclient-py>=1.8.0",
# Image, Video & Multimodal Perception # Image, Video & Multimodal Perception
"pillow>=10.0.0", "pillow>=13.0.0",
"opencv-python>=4.9.0.80", "opencv-python>=4.9.0.80",
"imageio-ffmpeg>=0.5.1", "imageio-ffmpeg>=0.5.1",
"matplotlib>=3.8.0", "matplotlib>=3.8.0",
@@ -67,7 +68,7 @@ dependencies = [
"scipy>=1.13.0", "scipy>=1.13.0",
# System, Utilities & Telemetry # System, Utilities & Telemetry
"psutil>=5.9.0", "psutil>=5.9.0",
"filelock>=3.20.0", "filelock>=4.2.4",
"retry>=0.9.2", "retry>=0.9.2",
"posthog>=3.5.0", "posthog>=3.5.0",
"uuid-utils>=0.9.0", "uuid-utils>=0.9.0",
+75
View File
@@ -0,0 +1,75 @@
"""Regression coverage for the prototype's python-jose to PyJWT migration."""
from datetime import UTC, datetime, timedelta
import importlib.util
from pathlib import Path
import sys
from types import ModuleType, SimpleNamespace
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
import jwt
import pytest
@pytest.fixture
def auth(monkeypatch):
# Load only the authentication module, without Docker, GCP or real settings.
config = ModuleType("app.config")
config.settings = SimpleNamespace(
JWT_SECRET_KEY="test-only-signing-key-with-at-least-32-bytes",
JWT_ALGORITHM="HS256",
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60,
)
monkeypatch.setitem(sys.modules, "app.config", config)
path = (
Path(__file__).resolve().parents[2] / "playground/backend_manager/app/auth/jwt_handler.py"
)
spec = importlib.util.spec_from_file_location("backend_manager_jwt_test", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.asyncio
async def test_signed_token_preserves_claims_and_authenticated_user(auth):
token = auth.create_access_token("user@example.com", {"role": "tester"})
claims = auth.decode_access_token(token)
assert claims["sub"] == "user@example.com"
assert claims["role"] == "tester"
assert claims["iss"] == "artemis-backend-manager"
assert claims["exp"] > claims["iat"]
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
assert await auth.get_current_user(credentials) == "user@example.com"
@pytest.mark.parametrize("invalid", ["expired", "signature", "algorithm", "unsigned", "malformed"])
def test_invalid_tokens_remain_unauthorized(auth, invalid):
claims = {"sub": "user", "exp": datetime.now(UTC) + timedelta(minutes=5)}
key = auth.settings.JWT_SECRET_KEY
algorithm = "HS256"
if invalid == "expired":
claims["exp"] = datetime.now(UTC) - timedelta(minutes=5)
elif invalid == "signature":
key = "different-test-only-signing-key-with-32-bytes"
elif invalid == "algorithm":
algorithm = "HS384"
key = "another-test-only-key-long-enough-for-hs384-signatures"
elif invalid == "unsigned":
algorithm, key = "none", None
token = jwt.encode(claims, key, algorithm=algorithm)
if invalid == "malformed":
token = "not-a-jwt"
with pytest.raises(HTTPException) as error:
auth.decode_access_token(token)
assert error.value.status_code == 401
assert error.value.headers == {"WWW-Authenticate": "Bearer"}
@pytest.mark.asyncio
async def test_missing_subject_is_rejected(auth):
token = jwt.encode({}, auth.settings.JWT_SECRET_KEY, algorithm="HS256")
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
with pytest.raises(HTTPException) as error:
await auth.get_current_user(credentials)
assert error.value.status_code == 401
+53 -8
View File
@@ -5,7 +5,7 @@ import json
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import httpx import httpx2 as httpx
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
from langchain_core.runnables import RunnableLambda from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, SecretStr from pydantic import BaseModel, SecretStr
@@ -59,26 +59,46 @@ def response_stream(status, arguments, name):
] ]
if status: if status:
events.append({"type": "response." + status, "response": body}) events.append({"type": "response." + status, "response": body})
return "".join("data: " + json.dumps(event) + "\n\n" for event in events) + "data: [DONE]\n\n" return (
"".join("data: " + json.dumps(event, ensure_ascii=False) + "\n\n" for event in events)
+ "data: [DONE]\n\n"
)
class FragmentedEventStream(httpx.AsyncByteStream):
"""Split SSE frames and multibyte UTF-8 characters at transport boundaries."""
def __init__(self, text):
self.data = text.encode("utf-8")
async def __aiter__(self):
for offset in range(0, len(self.data), 4093):
yield self.data[offset : offset + 4093]
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def transport_case(monkeypatch, tmp_path): async def transport_case(monkeypatch, tmp_path, request):
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
import openai import openai
requests, outputs, clients, events = [], [], [], [] requests, outputs, clients, events = [], [], [], []
def handle(request): def handle(http_request):
payload = json.loads(request.content) payload = json.loads(http_request.content)
requests.append(payload) requests.append(payload)
status, arguments = outputs.pop(0) status, arguments = outputs.pop(0)
name = payload.get("tools", [{"name": "finish"}])[0]["name"] name = payload.get("tools", [{"name": "finish"}])[0]["name"]
if payload.get("stream"): if payload.get("stream"):
stream = response_stream(status, arguments, name)
body = (
{"stream": FragmentedEventStream(stream)}
if getattr(request, "param", None) == "fragmented"
else {"text": stream}
)
return httpx.Response( return httpx.Response(
200, 200,
text=response_stream(status, arguments, name),
headers={"content-type": "text/event-stream"}, headers={"content-type": "text/event-stream"},
**body,
) )
return httpx.Response(200, json=response_body(status, arguments, name)) return httpx.Response(200, json=response_body(status, arguments, name))
@@ -175,15 +195,40 @@ async def test_completed_stream_returns_original_valid_arguments(transport_case)
assert result.tool_calls[0]["args"] == {"passed": True} assert result.tool_calls[0]["args"] == {"passed": True}
@pytest.mark.asyncio
@pytest.mark.parametrize("transport_case", ["fragmented"], indirect=True)
@pytest.mark.parametrize("status", ["completed", None])
async def test_large_unicode_tool_stream_requires_completion(transport_case, status):
case = transport_case
arguments = {"passed": True, "evidence": "截图已核验。\n" * 65536}
encoded = json.dumps(arguments, ensure_ascii=False)
assert len(encoded.encode("utf-8")) > 1024 * 1024
case.outputs.append((status, encoded))
if status == "completed":
result = await wrapped(case).complete([])
assert result.response_metadata["status"] == "completed"
assert result.tool_calls[0]["args"] == arguments
else:
with pytest.raises(LLMExhaustedError) as failure:
await wrapped(case).complete([])
assert failure.value.failure.category is FailureCategory.INVALID_OUTPUT
assert len(case.requests) == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("recovers", [False, True]) @pytest.mark.parametrize("recovers", [False, True])
@pytest.mark.parametrize("initial_status", [None, "failed"])
async def test_integrity_retry_is_bounded_and_keeps_existing_tool_evidence( async def test_integrity_retry_is_bounded_and_keeps_existing_tool_evidence(
transport_case, recovers transport_case, recovers, initial_status
): ):
case = transport_case case = transport_case
case.endpoint.max_retries = 5 case.endpoint.max_retries = 5
case.outputs.extend( case.outputs.extend(
[(None, '{"passed":false}'), ("completed" if recovers else None, '{"passed":true}')] [
(initial_status, '{"passed":false}'),
("completed" if recovers else initial_status, '{"passed":true}'),
]
) )
messages = [ messages = [
HumanMessage(content="Inspect then verify"), HumanMessage(content="Inspect then verify"),
Generated
+545 -390
View File
File diff suppressed because it is too large. Load diff