fix(deps): upgrade OpenAI stack and resolve dependency vulnerabilities
This commit is contained in:
1 parent
bb873ec0e0
commit
6b2c0f199d
14 files changed
+2737
-2731
No files matched your search
Generated
+1856
-2311
File diff suppressed because it is too large.
Load diff
@@ -9,24 +9,29 @@
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"overrides": {
|
||||
"@angular/build": {
|
||||
"vite": "8.3.5"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^22.1.5",
|
||||
"@angular/common": "^22.1.4",
|
||||
"@angular/compiler": "^22.1.4",
|
||||
"@angular/core": "^22.1.4",
|
||||
"@angular/forms": "^22.1.4",
|
||||
"@angular/platform-browser": "^22.1.4",
|
||||
"@angular/platform-browser-dynamic": "^22.1.4",
|
||||
"@angular/router": "^22.1.4",
|
||||
"@angular/cdk": "^22.2.4",
|
||||
"@angular/common": "^22.2.3",
|
||||
"@angular/compiler": "^22.2.3",
|
||||
"@angular/core": "^22.2.3",
|
||||
"@angular/forms": "^22.2.3",
|
||||
"@angular/platform-browser": "^22.2.3",
|
||||
"@angular/platform-browser-dynamic": "^22.2.3",
|
||||
"@angular/router": "^22.2.3",
|
||||
"marked": "18.1.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^22.1.6",
|
||||
"@angular/cli": "^22.1.6",
|
||||
"@angular/compiler-cli": "^22.1.4",
|
||||
"@angular/build": "^22.2.4",
|
||||
"@angular/cli": "^22.2.4",
|
||||
"@angular/compiler-cli": "^22.2.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"istanbul-lib-instrument": "^6.0.3",
|
||||
"jasmine-core": "~5.6.0",
|
||||
|
||||
@@ -30,6 +30,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import random
|
||||
import re
|
||||
from functools import lru_cache
|
||||
import threading
|
||||
import time
|
||||
@@ -124,6 +125,11 @@ def classify_failure(error: BaseException) -> Failure:
|
||||
# Programmer/request errors: retrying is useless and falling back only
|
||||
# hides the bug inside a different model's output.
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 开发交接: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-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)
|
||||
@@ -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)
|
||||
@@ -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 fastapi import Depends, HTTPException, Security, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
import jwt
|
||||
from jwt import InvalidTokenError
|
||||
from app.config import settings
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
@@ -40,7 +41,7 @@ def decode_access_token(token: str) -> dict:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
|
||||
return payload
|
||||
except JWTError as e:
|
||||
except InvalidTokenError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid or expired credentials: {str(e)}",
|
||||
|
||||
@@ -2,7 +2,7 @@ fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.32.0
|
||||
pydantic>=2.9.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
|
||||
docker>=7.1.0
|
||||
google-cloud-bigquery>=3.26.0
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# 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
|
||||
RUN apk add --no-cache openssl curl bash
|
||||
|
||||
+8
-7
@@ -27,14 +27,15 @@ dependencies = [
|
||||
# Remote Python SDK (kept as a zero-dependency workspace package)
|
||||
"artemis-client>=0.1.0",
|
||||
# Orchestration & Framework
|
||||
"langgraph>=1.0.2,<2.0.0",
|
||||
"langchain>=1.0.0",
|
||||
"langchain-core>=1.0.0",
|
||||
"langgraph>=1.2.18,<2.0.0",
|
||||
"langchain>=1.4.7,<2.0.0",
|
||||
"langchain-core>=1.7.0,<2.0.0",
|
||||
"langchain-community>=0.4.1",
|
||||
"langchain-mcp-adapters>=0.2.0",
|
||||
"mcp>=1.26.0",
|
||||
"mcp>=1.30.0,<2.0.0",
|
||||
# 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
|
||||
"pydantic>=2.7.0",
|
||||
"pydantic-settings>=2.10.1",
|
||||
@@ -59,7 +60,7 @@ dependencies = [
|
||||
"websockets>=12.0",
|
||||
"sseclient-py>=1.8.0",
|
||||
# Image, Video & Multimodal Perception
|
||||
"pillow>=10.0.0",
|
||||
"pillow>=13.0.0",
|
||||
"opencv-python>=4.9.0.80",
|
||||
"imageio-ffmpeg>=0.5.1",
|
||||
"matplotlib>=3.8.0",
|
||||
@@ -67,7 +68,7 @@ dependencies = [
|
||||
"scipy>=1.13.0",
|
||||
# System, Utilities & Telemetry
|
||||
"psutil>=5.9.0",
|
||||
"filelock>=3.20.0",
|
||||
"filelock>=4.2.4",
|
||||
"retry>=0.9.2",
|
||||
"posthog>=3.5.0",
|
||||
"uuid-utils>=0.9.0",
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import httpx2 as httpx
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
from pydantic import BaseModel, SecretStr
|
||||
@@ -59,26 +59,46 @@ def response_stream(status, arguments, name):
|
||||
]
|
||||
if status:
|
||||
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
|
||||
async def transport_case(monkeypatch, tmp_path):
|
||||
async def transport_case(monkeypatch, tmp_path, request):
|
||||
from langchain_openai import ChatOpenAI
|
||||
import openai
|
||||
|
||||
requests, outputs, clients, events = [], [], [], []
|
||||
|
||||
def handle(request):
|
||||
payload = json.loads(request.content)
|
||||
def handle(http_request):
|
||||
payload = json.loads(http_request.content)
|
||||
requests.append(payload)
|
||||
status, arguments = outputs.pop(0)
|
||||
name = payload.get("tools", [{"name": "finish"}])[0]["name"]
|
||||
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(
|
||||
200,
|
||||
text=response_stream(status, arguments, name),
|
||||
headers={"content-type": "text/event-stream"},
|
||||
**body,
|
||||
)
|
||||
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}
|
||||
|
||||
|
||||
@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.parametrize("recovers", [False, True])
|
||||
@pytest.mark.parametrize("initial_status", [None, "failed"])
|
||||
async def test_integrity_retry_is_bounded_and_keeps_existing_tool_evidence(
|
||||
transport_case, recovers
|
||||
transport_case, recovers, initial_status
|
||||
):
|
||||
case = transport_case
|
||||
case.endpoint.max_retries = 5
|
||||
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 = [
|
||||
HumanMessage(content="Inspect then verify"),
|
||||
|
||||
Reference in new issue
Block a user