Commit Graph

21 Commits

Author SHA1 Message Date
ThearchyHelios 5318c33f57 合入 nr8/b 的 ac8b194c(复核 R-b1–R-b6、交互留底的运行名额、先占额度再出具、直连地址 AGENTLAB_DATABASE_DIRECT_URL 和 AGENTLAB_DB_PGBOUNCER、不许新旧版本混跑、prod.sh 和 Docker 入口问 python -m app.cluster workers、镜像带构建号);没有冲突,README、config.py、db/base.py 自动合并 2026-10-19 14:01:07 +08:00
ThearchyHelios 93cd32bc97 启动时等库:PostgreSQL 还连不上(和库的容器一起起、库还在初始化)时按退避重试(1、2、4、5、5… 秒),每次照实打一条「应用库还没就绪」,最多等 AGENTLAB_DB_STARTUP_TIMEOUT(缺省 90)秒再把最后一次的错抛出去;密码不对、库不存在这种等也没用的(SQLSTATE 28、3D、42 类)立刻抛;SQLite 不等。新增 python -m app.ops(部署脚本在容器里以镜像用户调的几件小事,只用标准库):内置 PostgreSQL 的密码文件和 pgpass(0600,已有就沿用,密码不打印)、数据目录用哪种库的状态(sqlite / builtin / migrating)、SQLite 库的一致备份(备份接口,含 -wal 里的)和只留最近 N 份。copy-to-postgres 连不上目标库、库报错时一句话说清楚、退出码 1,不再甩一屏调用栈 2026-10-19 13:43:32 +08:00
ThearchyHelios d68ca9774f 加直连地址 AGENTLAB_DATABASE_DIRECT_URL 和 PgBouncer 事务模式开关 AGENTLAB_DB_PGBOUNCER(用户拍板):依赖会话状态的连接——收发进程间通知的那一条(LISTEN)、跨进程的锁(会话级咨询锁,另建一个直连的小池子)、断点库(缺省)——走直连地址,普通查询走 AGENTLAB_DATABASE_URL,不设就全用同一个;开关打开时普通地址不用客户端连接池、关掉 SQLAlchemy 和 asyncpg 的预备语句缓存、预备语句用随机名字(照 SQLAlchemy 文档),事务级咨询锁照常成立;开关打开却没给直连地址时拒绝启动。README 写三种部署(直连、PgBouncer 会话模式、事务模式加直连地址)和要配的 server_reset_query,照实说没在真的 PgBouncer 上跑过。补测试:地址解析和规范化、断点库缺省走直连、事务模式关掉的几样、LISTEN 和跨进程锁走直连、事务模式没给直连地址拒绝启动 2026-10-19 13:01:21 +08:00
ThearchyHelios 4e3623885b 合入 W2-a 的复核修复(nr8/a:导入的 NOT NULL 补值与孤儿行、收尾 dispose、sslmode 换算、PostgreSQL 写入去 NUL、不分大小写搜索 contains_ci、json_eq 两库同语义、JSON 守卫、用户可见列表补决胜键、bubblewrap --clearenv、部署脚本与原子写的几处、README 连接数预算)
# Conflicts:
#	backend/app/api/schedules.py
#	backend/app/main.py
2026-10-19 12:21:38 +08:00
ThearchyHelios db328efd21 原子写和部署脚本的几处(复核 R-a11、R-a12、R-a13 追加写)
- R-a12 create_bytes:硬链接不了的文件系统(exFAT、一部分 SMB / FUSE,EPERM / ENOTSUP / EXDEV)不再首次启动就崩,退回 O_CREAT | O_EXCL 直接建(仍然只建一次,说明里写了写的那一会儿读的一方可能读到不完整);建成后 fsync 目录(同 raw_store)
- R-a13 file_write 的追加改回原地追加写(O_APPEND,core/atomic.append_bytes):不换文件(inode、权限、开着的句柄不变),原有内容不动,文件大了也不用整份读出来重写;取舍写在函数说明里——读的一方最多看到半截新内容。覆盖照旧先写临时文件再改名,并沿用原文件的权限
- R-a11 deploy.sh 改属主前认数据目录收紧:只认 agentlab.db、.secret_key、.agentlab-data 三样之一(不再认 uploads/ 这种普通目录名);.agentlab-data 是服务启动时(Settings.ensure_dirs)建的标记,只建一次不覆盖,库在 PostgreSQL、密钥由环境变量给的部署也认得出;老部署有 agentlab.db 或 .secret_key。deploy/README 写明这个文件别删
- 测试:模拟不能硬链接仍只建一次、权限 0600、不留临时文件;覆盖沿用原权限;追加不换 inode、权限不变;file_write 追加不换文件、覆盖照常;数据目录里有标记、重复启动不覆盖
2026-10-19 12:07:31 +08:00
ThearchyHelios c768c56fd7 多进程协调补齐几处:执行时长从拿到名额算起(排队等名额不算执行,墙钟本来就从开始执行算);蓝图留痕、蓝图助手开轮先占住那一行再取最大值往下接;问数据等别的进程接着跑的运行时回库里等封存;按配方导入的 AI 起草、知识库重建索引另取跨进程的锁(多进程时重建进度记在设置表里,别的进程读得到、属主不在了照实说没做完);调度循环一拍的间隔可配(AGENTLAB_SCHEDULER_TICK_S) 2026-10-19 11:51:29 +08:00
ThearchyHelios 87f0d62f9b 多进程协调(W2-b)第一步:新增 app/cluster——工作进程在库里登记、心跳续租约(属主随进程,进程没了它认领的行一起到期),调度当班的租约,发给属主进程的停止 / 记事件请求,PostgreSQL 的 LISTEN/NOTIFY 叫醒,跨进程的互斥锁(咨询锁),SQLite 单进程守卫(第二个进程拒绝启动);运行、问数据一轮、起草 / 剖析 / 探查 / 评测 / 影子起草、文档、发布单记 owner,启动整理改成只接手属主不在了的行,当班进程隔一会儿再接手一次;运行的并发名额改成库里按交互 / 应用 / 定时 / 系统四类发(各有全站上限和优先级,设置里可调),替掉进程内的信号量;同源队记在库里;运行停止、记事件跨进程交给属主;运行事件、问数据事件跨进程转发(库里按 seq 追,不落库的流式增量和保活心跳只转给在看的进程);每日花费账先占住那一行再读写;访问密钥限流、登录失败计数在 PostgreSQL 时记在库里;登录设置、对外地址、向量模型、能力快照、数据源连接池、应用页面缓存跨进程失效;上传版本库、记录表、按需探查、出具、召回、补算、按需视图、个人刷新、定时发起的锁换成跨进程的锁 2026-10-19 11:40:33 +08:00
ThearchyHelios 2e7d6103ea 应用库可移植到 PostgreSQL:AGENTLAB_DATABASE_URL 设成 postgresql+asyncpg://… 时整个应用库用 PostgreSQL,断点用官方的 langgraph-checkpoint-postgres(同一个库,AGENTLAB_CHECKPOINT_URL 可另给);不设时照旧是数据目录里的 SQLite,行为不变
- 配置:database_url、checkpoint_url(启动时校验写法,只认 PostgreSQL)、连接池四项;启动日志写用的是哪种库(主机、库名、用户,不写密码)
- 引擎(db/base.make_engine):SQLite 照旧 WAL、外键、忙等;PostgreSQL 用连接池、探活、会话时区 UTC,装上方言调整(db/postgres.py):建表时 String(n) 是不限长的 VARCHAR(SQLite 从不查长度,导过来的存量也可能超长)、ORDER BY 里可空列的 NULL 位置照 SQLite 补写(升序在前、降序在后,窗口函数里同样);Integer 列在 PostgreSQL 上是 BIGINT(列挂到表上时换成 with_variant,绑定参数跟着列按 BIGINT 转换);JSON 列两边都存原文(PostgreSQL 的 json,不用 JSONB:JSONB 重排对象里的键,快照的列序、表的先后读回来就变了);JSON 里的 NaN 写成 null、NUL 去掉(PostgreSQL 不收)
- 迁移按方言:已有的列和索引用 inspector 认;PostgreSQL 上补列的类型取模型里这一列的、缺省值取迁移表里的(布尔 0/1 换成 false/true),ADD COLUMN IF NOT EXISTS;去重语句里的布尔按方言写;部分唯一索引补 postgresql_where;init_db 在 PostgreSQL 上拿咨询锁(几个进程同时启动不撞)
- 应用库上的 SQLite 专用写法改成按方言编译的表达式(db/portable.py):json_extract 的十几处(口径召回、出具、对账、个人应用、幂等键、访问控制、数据源变化通知、剖析任务、待办超时)换成 json_eq / json_flag / json_text,json_type 换成 json_is_string(两种库同一套带类型的比较);能力登记的 sqlite insert 换成按会话方言的 insert_ignore;能力使用次数的 json_each 按方言分支;附件「没绑记录」的条件不靠 json 的等号;按报错文字认唯一约束的地方换成 unique_violation(按表和列认,两种库的报错都认得)
- 断点(engine/checkpoints.py):PostgreSQL 时开 psycopg 连接池、在咨询锁里 setup;runner 只认 BaseCheckpointSaver,读断点仍只走 saver 的公开接口
- 依赖:新 extra postgres(asyncpg、psycopg[binary] 3、psycopg-pool、langgraph-checkpoint-postgres 3.1.2);langgraph-checkpoint 卡在 <4.3(断点格式就在这个包里,postgres 包新版要 4.3,不动已装的 4.2.0);锁文件补上新包
- 运行收尾:记 action.unknown、安排核实做完才把运行摘掉(以前先摘掉,等「不在跑了」再读事件的一方在库慢时读不到这一条)
- 测试:AGENTLAB_TEST_DATABASE_URL 时跑在 PostgreSQL 上(每个 xdist 进程建自己的库、按事件循环分的连接池 tests/pgpool.py),不设时盖掉环境里的数据库地址照旧用 SQLite;scripts/test-postgres.sh 起临时的 postgres:16 容器跑全量;sqlite_only 标记;另开连接改库的几个竞态测试、触发器测试改成两种库都能跑(tests/appdb.py);新增 test_db_portable 守着方言调整、JSON 键序、超长字符串、大整数、NULL 排序、时区、PostgreSQL 老库迁移
2026-10-19 09:52:09 +08:00
ThearchyHelios d8034ca71a 对外访问地址只留一个:core/public_url.py 的 public_base_url(),通知、Webhook 链接、OIDC 回调、来源核对都改用它
以前有两个「对外地址」:环境变量 AGENTLAB_PUBLIC_URL 只管 OIDC 回调和 Origin 核对,设置里 notify.public_base_url 只管
通知里的链接。只配了环境变量的部署,通知里不带链接;只在设置里填了的,回调又按请求的 Host 拼——违背「一种说法」。

- 新增 core/public_url.py:环境变量优先,其次设置(沿用 notify.public_base_url 这个键),都没有是 None。设置的值缓存在
  进程里(中间件按请求核对来源,不能每次查库):启动时 refresh 一次,PUT /api/settings 写完库就 remember,不用重启;
  通知、定时任务 Webhook 手里本来就有会话,用 refresh 取,顺手按库里的值对齐缓存
- 地址的形状检查和规范化只有一份(url_problem / normalize),设置接口的中文报错和环境变量的启动校验都用它;环境变量
  写错了(没有 http:// 或 https://、带 ? 或 #)启动时就报错,不再拼出坏的回调地址
- GET /api/settings 另给只读的 public_url 一组 {value, env_locked},设置页据此显示「由环境变量指定」;环境变量设了时
  PUT 把这一项改成别的值回 409(public_url_env_locked,field 指到这一项),原样写回不算改;public_url 一组写不进库
- notify/service 取地址的那一处、scheduler/webhook 链接里的地址、api/auth 的 OIDC 回调、auth/middleware 的 Origin 核对
  都改走 public_base_url;deploy/agentlab.env 把这个变量的说法改成全站唯一的对外访问地址

测试 tests/test_public_url.py:先后顺序(环境变量 > 设置 > 无)、规范化、启动校验、PUT 刷新缓存、环境变量锁定(409、
不挡原样写回)、只配环境变量时通知带链接、OIDC 回调和 Origin 核对认设置里的地址。去掉修复回跑,只配环境变量的通知
链接、按设置核对 Origin 两条都会失败。设置页把它挪到「通用」在后面的前端提交里。
2026-10-16 10:18:12 +08:00
ThearchyHelios a05efbf27a 安全响应头:所有 HTTP 响应补 nosniff、Referrer-Policy 和 CSP frame-ancestors,页面只上报严格的 script-src
以前服务端一个安全头都不发,任何站点都能把 AgentLab 嵌进 iframe:同站门户(Lax 会话 cookie 会随请求带上)和关登录的部署
都有被点击劫持的可能,也没有「允许谁嵌」的白名单可配。

- 新增 app/web_security.py,纯 ASGI 中间件,main.py 里加在最外层:CORS 预检、登录检查自己回的 401 / 403 也带上;
  WebSocket 不经过它;接口自己设过的同名头(文件下载的 nosniff)不覆盖、不重复
- frame-ancestors 缺省只有 'self',这时另加 X-Frame-Options: SAMEORIGIN;AGENTLAB_FRAME_ANCESTORS 配了同站门户的白名单时
  只发 CSP(XFO 写不了白名单,两个都发会让只认 XFO 的老浏览器把门户也拦掉)
- 分享页 /s 只认 AGENTLAB_EMBED_ORIGINS、Referrer-Policy 用 no-referrer。分享链接第 3 波才加,这一波先把配置读进来、
  头的规则写好
- 严格的 script-src 先只发 Content-Security-Policy-Report-Only:mount_web 启动时给构建产物 index.html 里的内联主题
  脚本算 sha256,只在页面响应上发;接口返回的 HTML(导出的报告自带 meta CSP)不发、不动。观察一个版本没有误报再强制执行
- HSTS 交给终结 https 的反向代理
- 两个白名单只收「协议://主机[:端口]」(可以 *. 开头),带路径、裸 *、没写协议的启动时就报错;deploy/agentlab.env 写明
  两个变量和缺省行为

测试 tests/test_web_security.py:页面、/api、中间件自己的 401、CORS 预检都带头;白名单生效时不发 XFO;/s 的规则;
上报策略只在页面上、哈希对得上;已有的头不重复;WebSocket 不受影响;白名单写错启动报错;子进程起真实的 app.main 核对
环境变量接线。
2026-10-16 10:17:37 +08:00
ThearchyHelios 5dba66c2b6 应用平台 BE-1 治理底座:身份三种模式(off 默认 / local / oidc)、会话与 CSRF、中间件写入身份、角色与组与审计、按权限矩阵挂路由级依赖、运行与工件的读权限和 user 角色遮罩、reset-admin 找回;能力登记(同步、治理与乐观锁、发起快照、发布版本、作证判定)、登记的端点与平台执行器(分环境、演练、幂等头、身份断言、端点快照作证)、密钥加密与掩码、MCP 的 SSRF 与认证头、写入能力只进调用工具节点、正式运行里只读免审批的能力直接执行、四条 action.* 发布检查(含统一复核后的修复) 2026-10-14 20:05:23 +08:00
ThearchyHelios a3058e7df8 复核套件骨架(后端):先把后续几个工作包要用的位置占好,各包只改自己的文件。节点类型加双盲复算 dual_check、反方审稿 challenge,执行器是占位(记一条 info 日志后跳过,产出 skipped 和原因),校验按普通节点走,叫法进 labels,暂不写进助手的节点说明。出具契约留一个通用口子:成果节点上游任何节点产出里的 issuance_gaps(字符串列表)收进缺口,前面加上「节点类型「节点名」:」,出具随之降档、不会升档;只认这个成果的上游,写坏的条目跳过。路由先登记 schedules、compare、bundle、inquiry、whatif 五个空前缀,挂在 /api/runs 下的四个排在 runs.router 之前,以后加固定路径不会被 /{run_id} 吃掉。定时任务的调度先放空的 start / stop,AGENTLAB_SCHEDULER 默认开、0 / false / off 关,写进设置;启动时开着才 start,测试环境默认关 2026-10-04 23:40:20 +08:00
ThearchyHelios 6af4e396b9 生产部署:prod.sh 直接运行,package-docker.sh 打 Docker 离线包(镜像 + 一键部署脚本 + 配置 + 说明)
- 服务端可以托管前端构建产物(AGENTLAB_WEB_DIST):页面路由回退到 index.html,/api 下不存在的路径照旧是 JSON 404,
  挡住路径穿越;index.html 不缓存,assets/ 带哈希的文件长缓存;没设时行为完全不变
- scripts/prod.sh:不用 Docker 的生产运行。按需构建前端,单进程、不热重载,默认只听本机,--host 开放到局域网并提示没有登录
- Dockerfile(多阶段构建,Linux 镜像):前端在构建机架构上构建,运行阶段用 python:3.12-slim,依赖装进独立的 venv,
  以非 root 用户(uid 10001)运行,数据在 /data,带健康检查;.dockerignore 排除 .env、data/、各种库文件,镜像里实测没有密钥和数据库
- scripts/package-docker.sh:构建指定平台的镜像(默认按本机架构,可选 linux/amd64),打成
  release/agentlab-<版本>-linux-<架构>.tar,里面有 image.tar、deploy.sh、agentlab.env、README.md、SHA256SUMS
- deploy/deploy.sh:目标机上只依赖 Docker,离线可用。install / start / stop / restart / status / logs / upgrade / uninstall;
  检查架构和配置,自动导入镜像,替换容器时保留旧容器以便回退,数据永远保留,日志轮转
- 修正 bubblewrap 的可用性判断:以前只看有没有装 bwrap,容器里和 Ubuntu 24.04 默认配置下装了却建不了命名空间,
  每次执行代码都失败;现在真起一次试,不能用就退回下一个后端,并写明原因
- README 增加「生产部署」一节,部署细节见 deploy/README.md
2026-09-30 09:48:16 +08:00
ThearchyHelios 3e311ec551 第三波收尾:跨模块整合、全站扫尾、检查脚本整合;网页工具认得本机代理的 fake-ip
整合(3c-integrate)
- 公共层:跨天时长(formatSpan / formatOffset / formatLapse)、本机署名(lib/actor)、检索分类色令牌、
  数据源目录(useDatasources)收进公共层;失败说明能一次列出多个写坏的自定义工具,查询超时指到数据源的查询时限
- 管理页收尾:设置页接入统一的离开前确认;检查脚本不再把沙箱里的真实值打进输出
- 画布 ↔ 右栏:点出具印章、点节点能跳到右栏对应位置;「定位」直接聚焦到出问题的字段
- 回答流 / 问数据 / 记录页:失败信息带「去哪修」且不再被翻译两遍;协作团队的结局按执行截取;
  等审批时断流重连、而运行已在空档里跑完的,补全事件

扫尾(3c-sweep)
- 术语统一:开始运行 / 继续运行、工作流最大步数;未保存的运行统一叫「未保存的工作流」(后端新运行同步)
- 沙箱报错不再带 Python 异常类名;新增 check-tokens,未定义的 CSS 变量为 0,两个旧别名迁移后删除

检查脚本(3c-checks)
- check-ui 跟上新界面(运行列表按 data 属性找行),加上令牌与选中态的回归断言,输出里的名字打码
- 新增 check-all:按顺序跑全部检查、汇总退出码,--repeat 列出偶发项
- 单节出错只记这一节失败、不再带崩整个脚本;外壳遥测浮层「重新检测」时焦点不再掉到 body

网页工具与 fake-ip
- Clash / mihomo 一类代理开 TUN + fake-ip 时,查什么都先拿到 198.18 段的假地址,被当成内网拦截。
  新增 AGENTLAB_HTTP_TOOL_FAKE_IP_RANGES(默认空):落在段里的解析结果改向公共 DNS(DoH)核实真实地址,
  公网放行、内网照拦、核实不了按拦截处理。DoH 默认先问境外的,境内公共 DNS 会对被封锁的域名返回污染结果
- 列表型环境变量接受逗号分隔(原来注释说逗号分隔、实际只认 JSON 数组);域名解析改为异步,不再阻塞事件循环

验证(沙箱):tsc 干净,pytest 866 passed(两遍),检查脚本全绿。
2026-09-27 23:37:32 +08:00
ThearchyHelios 5928cc052e 三道只配不管的护栏开始干活:运行时限、模型超时、清单核对
显示成生效却没生效,比明说没有更危险——它让人以为有东西在兜底。

1. max_run_seconds 只存在于配置和设置接口里,没有任何代码执行它:一个挂住的
   模型或工具能把运行和它占着的并发名额一直拖着。现在 runner 用 asyncio.timeout
   强制执行,按"一段执行"计(等人工审批时 _drive 已结束,不算);超时判 failed、
   说清是哪道上限,可以接着跑。别处抛的 TimeoutError 不冒充这道上限(看
   Timeout.expired())。

2. 模型调用从来没设过超时:ModelSpec.timeout 从没被赋值,那条传参分支一次都没
   执行过,所有调用走 SDK 默认的 10 分钟外加重试。新增 model_timeout_seconds
   (默认 300 秒)作为缺省,节点显式配的仍优先;已确认 ChatAnthropic / ChatOpenAI
   都接这个参数。

3. manifest 哈希只写不验,而且是在 run.finished 落库之前算的——清单里恰恰少了
   宣布"跑完了、成果是什么"的那条,改成果清单照样对得上。现在终态事件落库之后
   再封存,并记下封到哪一条(manifest_seq,新列);新增 verify_manifest 与
   GET /api/runs/{id}/verify,运行详情页加了核对按钮。封存之后追加的事件不算篡改。
   老运行没有 manifest_seq,按当时的口径还原范围——开发库里 300 条带哈希的历史
   运行全部核对通过(成功 / 失败 / 取消各类都有),没有一条误报。

设置页「运行环境」补上单次执行时限和模型调用超时两项,README 加了对应的常见问题。

顺带修了一个被 _seal 放大的老问题:_drive 的收尾被打断时(关停取消、库连接出错)
漏掉 _tasks.pop,留下的任务让下一次 shutdown 的 gather 在别的事件循环里抛
"future belongs to a different loop"——测试里表现为每八九轮一次、一出就是几十个
teardown error。pop 移进内层 finally,shutdown 只等未结束的任务并清空。修后全量
连跑 12 轮 12 轮全绿。

test_guardrails.py 8 例:超时中止(挂住的节点 1 秒后被停下)、缺省模型超时、封存覆盖
run.finished、改成果被戳穿、事后追加不误报、老运行按旧口径核、中断态未封存、接口
404。414 → 422 passed。
2026-09-25 15:59:32 +08:00
ThearchyHelios 4057cdc35f 步数用尽时先收个尾,别把半截过程当答案交出去
走到步数上限一定意味着最后那步**要求了工具**(不要工具的话循环早 break 了),
也就是说模型从来没拿到过"说结论"的那一轮——它只是被掐断在半路。交出去的
是它上一句中间过程的话,末尾补一句"步数不够,请调大"。

实测这条线是绷紧的:库里 116 个 agent 节点,87% 在 10 步内收口,11 步只有
1 个,**正好 12 步的有 11 个**。1 跳到 11 是被截断的特征,不是自然分布。

所以补上那一轮:不给工具,让它基于已经查到的收口,并说清哪些没查到、
结论因此有什么局限。用 base_model 而不是绑过工具的 model——收尾轮必须在
结构上发不出 tool_call,靠提示词请它别调是约束不住的。

两种收场发不同的 code:收上来了是 step_limit_settled(DEGRADED),
收不上来仍是 step_limit(BROKEN)。不复用同一个 code:库里那 8 条老事件
的含义确实是硬截断,改语义等于把历史运行重新解释成另一回事。

顺带修掉一个一直在白烧钱的 bug:chat.ts:440 的注释说自动重试能"调大最大
步数",而 copilot.py 明确禁止模型写 max_steps。两边对不上,于是 step_limit
的重试是用同一个默认 12 再跑一遍,必然撞同一堵墙。现在那条禁令改成有条件
的——只有上一次死于步数不够时才准显式写大值。改完两条路各自成立:收尾
成功走 DEGRADED,闸门不开、不重试(对的,同配置再跑还是撞墙);收尾失败
仍是 BROKEN,而这次 Copilot 真能把步数调大。

硬顶 25 → 100,画布滑块跟着放开,**节点默认仍是 12**:要更多的人调得动,
不知情的人不会突然多花十倍。

新增 tests/test_agent_steps.py,顺带补回 agent 循环的集成测试(原来那个
文件在脱敏那轮被整个删掉了)。两条反向断言是重点:正常收口的 agent 一次
调用都不该多花,以及收尾轮发不出工具调用。两者都做过变异验证——把收尾
结果丢弃、把收尾轮换成绑工具的 model,各自都能让对应的断言变红。

273 passed(去掉一个会真连 api.openai.com 的既有慢测试是 238 passed / 9.4s)。
2026-09-24 16:38:26 +08:00
ThearchyHelios 6a46e4b55c 大文件传得进来了:分批算向量、上传转后台、边读边数大小
压测量出来的三个问题,10MB 文本(切 7914 段):

    上传耗时  150s(本地哈希)   换成 Qwen3-Embedding-4B 要 40 分钟以上
    而这 150 秒是压在一个 HTTP 请求里同步跑完的

浏览器和反向代理的默认超时都在 60 秒上下,也就是 5MB 以上的文件用户必然
看到超时——而后端其实还在跑,跑完文档也确实入了库。界面显示失败、数据其实
成功,比直接拒绝还糟。

**分批算向量。** 原来 ingest 是 embed_texts(pieces) 一次全发。本地哈希只是慢,
换成远端模型就是必挂:近八千段一次请求塞不下(OpenAI 单次上限 2048)。
reindex 那边早就分批了,ingest 这条一直漏着——而我上一轮刚把"换真模型"做成
设置页里点两下的事。

**上传转后台。** create_document / process_document 拆开,上传立刻返回
status=processing,切块和算向量在后台跑,进度写回文档行,前端轮询显示
"正在切块并算向量… 4288/7914 段"。实测上传返回 0.07 秒。

进度这件事返工过一次:先用另一个 session 单独写,结果整个过程界面上都是空的、
只在结束时闪一下——SQLite 只允许一个写者,主事务开着的时候那次写被堵住了。
改成主事务自己按批提交,顺手把进度写进去。

失败要写回 failed,否则永远停在"处理中"转圈。服务重启时也把残留的
processing 收尾成 failed——和 runner 对 running 的 run 是同一个道理。

**边读边数大小。** 原来是 raw = await file.read() 读完再判断,传个 1GB 的
文件内存在报 413 之前就吃掉了——那个检查拦的是"入库",不是"占内存"。改成
按块读、超了立刻停。上限从写死的 10MB 变成配置项,默认 50MB。

端到端验过:10MB 上传返回 0.07s,后台进度 768→1472→…→7914 实时可见,
最终 ready / 7914 段。
2026-09-20 09:47:02 +08:00
ThearchyHelios a16cdf3ab1 修两条 critical:文件工具可读任意路径、并发代码节点互相覆盖
## 文件工具可读任意文件(含加密主密钥)

sandbox_session 从请求体直接拼进工作区路径,没有任何净化。传 ".." 就能把
根目录挪出工作区,之后 _resolve 的越界检查是在这个被挪走的根下做的,等于失效:

  curl -X POST /api/tools/file_read/run -d '{"sandbox_session":"..","args":{"path":".secret_key"}}'
  → {"ok":true,"content":"8UygDbx9..."}    # 加密所有 provider Key 的 Fernet 主密钥

根因是同一段净化逻辑被抄了五遍:四个沙箱后端各一份,文件工具那份漏抄了。
所以提到 config.sanitize_session 统一一份,五处共用;RunToolIn.sandbox_session
再加 pattern 做纵深防御——畸形 session 直接 422,而不是悄悄净化成 default。

## 并发代码节点互相覆盖入口脚本

同一 run 的节点共用一个沙箱会话,而入口脚本写死 main.py。README 主打的
"连线即并行"会让并排的代码节点在同一 superstep 并发执行,都写 main.py 就
互相覆盖——后写的赢,每个节点却都拿到 ok=True 和"自己的"返回值。

  8 路并发同 session:7/8 执行了别人的代码,error 全为 None

改成每次执行生成唯一入口脚本(__entry_<uuid>.py),跑完即删。共享工作区语义
保留,跨节点传文件照常。四个后端同构改造,实测 8/16 路并发均 0 串味。

## 顺带修掉两个自己埋的坑

- conda 迁移把沙箱解释器隔离弄丢了:conda 环境 sys.prefix == sys.base_prefix,
  于是"干净解释器"挑出来的就是项目环境自己,沙箱代码能 import cryptography
  直接解密落库的 Key。改成实际探测候选解释器是否看得见敏感包,都不干净就
  退回 -S 关掉 site-packages。README 那句承诺现在重新成立了。
- -I 在 3.11+ 隐含 -P,脚本目录不进 sys.path,代码节点的 files 参数(多文件)
  在 seatbelt/bubblewrap 下一直是坏的,import helper 直接 ModuleNotFoundError。
  换成 -s -E,隔离意图不变,同目录 import 恢复。
- microvm 的 files_written 取 FsEntry.name(不存在),塞进去的是对象 repr。
  改取 path 的尾段。

沙箱回归 16 项通过(两档各 8 项)。
2026-09-14 15:22:58 +08:00
ThearchyHelios 7d00c24ab1 microVM 沙箱后端:内存限额终于真限得住,但网络关不干净
Seatbelt 在 macOS 上最难受的缺口是 RLIMIT_AS 不被强制——设 256MB 仍能分配
900MB。microVM 不是"更严格的访问控制",是换了一类边界:宿主 Darwin 27.0.0,
VM 里 Linux 6.12.99(libkrunfw),PID 1 是 init.krun,/Users 根本不存在,
free 只看得见 512MB。申请 900MB 会被 OOM killer 杀掉而 VM 照常存活。

实现按实测契约写,不按别的 SDK 的惯例推断:create() 直接返回已启动的
Sandbox,没有 handle/connect 两层;cwd/env/timeout 都是 exec 的原生参数;
超时抛 ExecTimeoutError 而不是返回非零码。

必须写明的缺口:network=false 时 HTTP/HTTPS 与域名解析都断得掉,但 UDP/53
拦不住,手写 DNS 包仍能拿到真实响应。default_egress=DENY、Rule.deny_dns()、
显式 deny UDP、max_connections=0 全试过都堵不上——deny_dns 生成的规则只针对
宿主,DNS 出口在 microsandbox 网络栈里是硬编码放行的。所以 health 里 network
标成 partial 而不是 true,设置页对应显示黄色「部分生效」:拦了一半要是显示成
全绿,比显示成全红更危险。

成本也照实说:首次拉 OCI 镜像实测 54.5s,之后热启动 0.19s、执行 7~30ms。
贵的只是第一次,所以 auto 在镜像没缓存时先用 Seatbelt,不让人干等一分钟;
显式选 strict 档不受此限——那是使用者自己要的硬隔离。

端到端 17 项验证通过:隔离、内存拦截、超时 124、退出码透传、多文件与 env、
会话间互不可见、VM 回收。Seatbelt 那档回归未受影响。
2026-09-14 14:10:10 +08:00
ThearchyHelios dd57c12016 沙箱改用系统自带的隔离原语,移除 Docker 依赖
跑一段不可信代码不需要一整套容器运行时。macOS 用 Seatbelt
(sandbox-exec,系统自带),Linux 用 bubblewrap;冷启动从 ~250ms
降到 ~25ms,且不再需要装任何东西。

守住三件事:默认断网、家目录不可读、只有工作区可写。策略由内核
强制且对子进程继承——沙箱内 subprocess 出来的 cat 读家目录同样是
Operation not permitted。解释器改用 sys.base_prefix 下那个干净的
Python,沙箱代码因此 import 不到后端的 FastAPI、SQLAlchemy 和凭据
处理代码。

如实标注能力边界,不给虚假的安全感:
- macOS 不强制 RLIMIT_AS,内存限不住(实测设 256MB 仍可分配 900MB),
  health 接口申报 enforced 字段,界面把限不住的项划掉
- Seatbelt 是访问控制而非虚拟化:进程表共享,以当前用户身份运行
- bubblewrap 侧多隔离 PID/IPC/挂载视图,内存限额真实生效

顺带修两个 bug:
- LocalSandbox 设 RLIMIT_NPROC=64 会误伤——macOS 上它按 uid 全局
  计数,用户已有上千进程时沙箱内任何 fork 都失败,连 bash 都起不来
- 模型所属 provider 被停用时,原先静默换一家继续用原模型名,必然
  404 且报错指不到原因;改为直接说清是哪个 provider 被停用了

写 SBPL 的坑记在注释里:(deny default) 会让 Python 直接 SIGABRT,
改用「默认允许 + 精确拒绝」。

7 个模板全部回归通过。
2026-09-13 18:04:36 +08:00
ThearchyHelios 964eeaa829 AgentLab:可视化 Agent 编排实验台
在画布上拖拽编排 agent 工作流,并实时看着它执行。

后端 FastAPI + LangGraph:把画布 JSON 编译成 StateGraph,15 种节点
(模型/Agent/多Agent、工具/沙箱、分支/循环/子图、记忆/检索/整形、
人工介入/结构校验)。选 LangGraph 是因为可视化编排真正难的是暂停、
恢复和回放,而 checkpointer + interrupt() 正好把这几件事做在了底层。

前端 React + React Flow:节点元数据驱动属性面板与连接桩,运行时
节点高亮、边流动画、卡片内流式 token 全部由同一份事件流驱动。

关键实现:
- Agent 内工具审批用 @task 缓存模型调用,interrupt 重放时不重复计费
- 沙箱保持只读根文件系统,代码经 exec+base64 写入(put_archive 会被
  只读容器拒绝),断网、非 root、内存/CPU/超时限额
- 检索为向量与 BM25 混合,本地哈希 embedding 零配置可用
- 分支条件走 AST 白名单求值器;HTTP 工具逐 IP 拦截内网与元数据地址
- Mock provider 让无 API Key 也能跑通完整链路

7 个内置模板全部冒烟通过,含真实模型端到端验证。
2026-09-13 17:27:00 +08:00