按 MIME、解析能力和 Provider capability 路由;音频、视频、图片和文本文档不共享一条假设链。
理解 LearnGraph
从一条真实调用链开始
LearnGraph 是一个目标驱动、证据驱动的学习图谱智能体。本页描述当前仓库中可验证的架构边界, 以及 Agent、渐进式工具、Skills、MCP、Docker 沙箱和系统提示词如何协同工作。
“已注册接口”不等于“外部服务已连通”,“支持 Provider”不等于“当前部署已配置”。 文中始终把代码能力、运行配置和真实远程验收分开描述。
Reading guide
实现状态与阅读约定
LearnGraph 仍处于早期开发阶段。开发者文档采用三种状态词,防止将未来设计或配置入口误解为已完成能力。
可在路由、服务、仓库或 Provider 适配器中追踪到完整调用链,并有持久化模型支撑。
代码已接入,但依赖部署配置、外部凭据、Docker 或远程服务;缺失时应显式返回 unavailable。
仅存在于总体架构或设计 TODO 中,不应作为当前 API 行为或验收结论。
设计共识→设计 TODO→总体架构→实现 TODO→OpenAPI / 模型 / 代码
Quick start
本地启动
根目录脚本负责编排 React/Vite 前端与 FastAPI 后端。默认数据库和对象存储均为本地实现。
# 安装依赖并启动(首次运行)
npm.cmd run dev:install
# 后续联合启动
npm.cmd run dev
# 静态检查、现有测试与生产构建
npm.cmd run check
http://127.0.0.1:5173http://127.0.0.1:8000http://127.0.0.1:8000/docs/api/v1/health
未配置模型 Provider 或远程调用失败时,服务必须明确报错。
确定性的本地演示 Provider 仅在 LEARNGRAPH_ENABLE_LOCAL_DEMO_PROVIDER=true 时启用,
不能作为真实验收证据。
Architecture
系统架构
当前实现是前后端分离的模块化单体。浏览器只调用 LearnGraph 后端;外部模型、搜索、抓取、研究、 MCP 和存储都由后端经 Port/Adapter 边界访问。
/api/v1
Bearer Session · Workspace Scope · RBAC / ACL · Error Envelope
后端依赖方向
业务服务不直接依赖厂商 SDK。新增外部能力应先定义或复用 Port,再提供适配器、能力探测和明确失败状态。
backend/app/main.py · backend/app/api/router.py · backend/app/providers/ports/Architecture evolution
从 v0.1 起步到可治理 Runtime
旧设计文档中的 c173b36 基线来自 rebase 前分支,不在当前 main 历史;当前 main 以 ddf3a1c(v0.1 首发)为起点,HEAD 57fd37f 把记忆系统、沙箱审批制出网、交互式子应用与产物版本化推进到可观察、可恢复、可审计的边界,并已完成一轮上线前全盘审计(NO_GO → 修复 → 有条件放行)。
建立首版媒体输入、转录路由、音视频附件策略、解析器、文件服务与验证脚本基线;失败从“泛化错误”变成可定位的能力问题。
长期记忆从“摘要 + 检索”升级为原子记忆能力(自动提取、按需回填、用户纠错优先),并切换为默认读取链路;事件溯源等底层架构仍在演进中。
沙箱 Egress 审批链全量落地并默认开启;上线前审计发现生产构建 P0 白屏、登录并发、越权等问题,逐项修复后按边界放行。
记忆系统、沙箱 readiness 与 egress 授权、原子工具、渐进披露、断线恢复、产物版本化与分享令牌、交互式子应用全部就位。
沙箱、工具、Grant、工作区和 Provider 都是独立边界;没有能力就明确 unavailable,不静默落回宿主。
数据库记录、事件、revision 和 audit 是事实;SSE、UI 面板和缓存都是可恢复或可重建的视图。
模型先看到能力摘要和授权范围,再在需要时发现工具、读取大结果或展开上下文,减少噪声与权限面。
枚举校验、能力门控、失败边界都要显式:要么显式报错,要么显式兜底,绝不静默改变语义(记忆摘要空值的教训)。
上线前审计(构建、并发、越权、限流、计费)与 CONDTIONAL_GO 边界是发布纪律,不是可选项。
v0.1 起步版
- 重点是媒体能否被正确解析和路由
- 附件策略与 Provider 失败边界开始显式化
- 验证以脚本和单次请求为主
记忆 + 治理版
- 输入、工具、沙箱和记忆均有稳定事实与生命周期
- 结果可流式呈现,也可从持久化记录恢复
- 每个高风险动作都有授权、审计和可拒绝路径
git show ddf3a1c · 当前 HEAD:57fd37f · 当前证据:backend/app/services/agent_runtime.py · backend/app/services/memory_event_store.py · backend/app/services/sandbox.py · backend/app/services/egress_approvals.pyMedia pipeline
媒体输入:转录、视觉与文档证据
媒体不是“把文件塞进 prompt”。当前输入管线先判断文件能力,再选择原生模型、转录 Provider、索引检索或外挂视觉描述,并把每一步的状态保留下来。
先验证 MIME、存储状态和大小,再走 transcription Provider;模型 ID 由能力快照和路由器统一决定,不能由前端拼接。
已确认原生视觉能力则直接发送;未知私有模型先探测,遇到明确“不支持图像”错误时记录 runtime observation 并回退视觉模型,描述按内容 hash 缓存。
可索引格式进入 chunk/retrieval;全文总结请求使用有上限的 full-document strategy,超出安全上下文时建议切换 Agent 分段读取,不静默截断。
媒体可直接预览不代表需要建立文档索引;同样,转录成功也不代表已建立可引用的 revision。显示能力、检索能力和回答证据必须分开。
backend/app/providers/remote/transcription.py · backend/app/services/document_parsers.py · backend/app/services/document_learning.py · backend/app/services/chat.pyRequest gateway
API 网关与请求边界
当前仓库没有独立的网关微服务。FastAPI 的统一 /api/v1 路由层就是应用网关:
它终止认证、解析工作区作用域、校验权限、稳定错误结构,并把请求交给领域服务。
网关职责
正式登录接口签发不透明 Bearer Session;浏览器不持有 Provider Secret。
X-Workspace-ID 只是请求提示,不能替代服务端授权。
普通资源使用 JSON;Chat 使用可恢复 SSE,但消息与事件仍落库。
Provider unavailable、权限拒绝与输入错误必须保持可区分,禁止伪造成功。
路由域
authdashboardgoalsgraphssessions
filesresearchsourcesevidenceexercises
memorymemory_v2episodeslearning_state
mcp-skillsprovidersusageimage_generations
pluginscomponentssubappsartifact_gateway
egress_approvalsfetch_authorizationsmigrations
auditworkflowsandboxdocument_learning
Durable facts
持久化与事实源
SQLite 是 MVP 的规范业务事实源。本地文件系统承载默认对象存储, 但文件路径、外部 UUID 或 SSE 事件都不能替代数据库中的稳定业务身份。
| 层 | 保存内容 | 关键边界 |
|---|---|---|
| SQLAlchemy / SQLite | 账号、RBAC、Goal、Graph、Session、Evidence、Provider、授权、审计 | 当前规范事实源 |
| Message timeline | Message、MessageVersion、MessagePart、SSE 事件、Provider Trace | SSE 只负责传输 |
| Memory | 原子记忆(提取 + 存储 + 按需回填) | 记忆是长期事实,可随用户纠错更新 |
| Object Storage | 上传文件、生成产物、内容寻址 Blob | 默认本地,可经迁移边界切换 |
数据库和对象存储切换需要维护锁、预检、校验、切换点与回滚;不能静默双写或降级。
Capability adapters
Provider 层
Provider 是外部能力网关,不是业务事实源。业务层依赖 Protocol 定义的 Port; 适配器负责协议差异、探测、超时、限额和 Provider Trace。
能力快照与按供应商门控
供应商协议差异(搜索参数、思考模式开关、图片输入、计费字段)被建模为能力快照,
而不是散落在调用点的 if。发不发 enable_search 由"该供应商是否支持"决定——
曾因向 OpenAI 兼容中继发送未知字段 enable_search 触发 400,修复后:
非 DashScope 中继一律不发;存量误标由 repair_non_dashscope_capabilities.py 修正。
极速模式的思考关闭同样按模型维护关闭矩阵(required / disabled / none / off),并拦截图片模型误作文本模型。
原生协议特化通道
DashScope 联网搜索的"返回搜索来源"(enable_source → search_info.search_results)与
"角标标注"(enable_citation + citation_format → 文本 [ref_N])仅原生协议支持,
OpenAI 兼容方式(Chat Completions 与 Responses)均不支持。QwenChatProvider 在
is_dashscope_origin 且开启 native_web_search、无 tools、纯文本时自动切原生通道
(POST /api/v1/services/aigc/text-generation/generation,X-DashScope-SSE: enable),
带 tools 的 Agent 回合与多模态保持兼容模式;prepend_search_result 可提前返回搜索来源降低首包延迟。
模型消息抽象
ProviderChatMessage 保留 system/user/assistant/tool 角色、tool calls、reasoning summary
与协议原生 response items。原生 continuation state 作为不透明数据保存,不能被业务层臆造或跨协议消费。
Provider 连接按计划缓存跨请求复用(避免每条消息重建 HTTP 客户端与 TLS 握手);usage 归一化拆分
input / cached / cache-creation,缓存 token 参与计费与预算(CNY/USD 多币种)。
backend/app/providers/ports/model.py · backend/app/providers/remote/ · backend/app/providers/catalog.py · backend/app/providers/qwen_catalog.py · backend/repair_non_dashscope_capabilities.pyAgent runtime
Agent 执行循环
Agent 模式是在持久化 Chat Session 上运行的受控工具循环。模型决定是否发起工具调用, 但工具可见性、授权、执行、结果裁剪、审计和持久化均由 LearnGraph 控制。
一次 Agent Turn
- 01持久化用户输入
用户消息、附件引用、所选节点与 Session 绑定先成为可追踪事实。
- 02构建授权上下文
只加入本次允许访问的节点、文件、选区和 Skill 指令;文档片段被标记为不可信参考数据。
- 03计算工具注册表
内置工具按角色开放;声明式 Skill 与 MCP 还必须具有 enabled 状态和持久
alwaysGrant。 - 04调用模型 Provider
适配器把结构化消息与 JSON Schema 工具定义发送给已配置远程模型。
- 05受控执行 Tool Call
服务端解析参数、重新校验作用域与授权、执行领域服务或 Provider,并保存 Invocation / Audit。
- 06继续或结束
工具结果作为
tool消息回填;模型可继续调用工具,最终文本和所有 MessagePart 一并持久化。
lg_graph_propose_change 只创建可审核的 GraphChangeSet 与组件,
不改变已发布图谱;Evidence 由 Agent 写入时保持 pending,不会直接授予 mastery。
Progressive disclosure
先给能力地图,再展开执行细节
渐进式披露不是隐藏能力,而是控制每一轮进入模型上下文的工具数量、描述体量和权限面。运行时先计算本轮能力地图,只把当前模式、用户角色、工作区 Grant、资源作用域和 Provider 状态允许的工具族放入候选集合;模型需要某项能力时,再读取其精确契约或调用按需发现工具。
工具族 · 状态 · 来源 · 授权范围
按需读取 schema · 限制 · 错误语义
权限复核 · 结果句柄 · 审计 · 产物
披露与调用的三层边界
普通 Chat 不注册函数;Agent 模式也只看到当前任务可能需要且已经启用的工具族。MCP 与 Skill 的旧描述不能替代当前 Capability Snapshot。
工具名称、JSON Schema、参数上限、错误语义和结果句柄按需展开;大结果优先返回摘要、文件或 Artifact 引用,不把完整 payload 塞回上下文。
工具对模型可见不代表一定可执行。每次调用仍由服务端重新验证 Membership、workspace permission、资源作用域、Grant 和 Provider readiness。
调用参数使用脱敏摘要持久化;成功、拒绝、超时、截断和 unavailable 都是规范结果,并与 MessagePart、Invocation 或 Sandbox Command 关联。
一个工具只负责一个可验证动作,例如读文件、写文件、列举文件、执行命令、转录或发布产物。组合逻辑留给 Agent;授权、幂等、审计和结果裁剪留在服务端。
backend/app/services/agent_runtime.py · backend/app/services/mcp_skills.py · backend/app/services/sandbox.pyAtomic capabilities
工具是权限边界,不是模型的万能遥控器
read_file、list_files、subapp_observe 等只读能力返回有限摘要,不直接暴露不可信 payload。
write_file、publish_file 等改变状态的能力需要明确作用域、大小限制和可追踪结果。
exec、transcribe_audio 运行在受控 Provider/沙箱中,保留超时、退出码、截断和审计。
删除、外发、网络访问等操作与 Grant/egress approval 绑定,拒绝也是完整结果。
| 原子工具属性 | 服务端要求 | 失败时行为 |
|---|---|---|
| 单一动作 | 读、写、列举、执行、转录、发布分别建模,不提供绕过策略的万能 shell 工具 | 返回稳定错误码,不猜测替代动作 |
| 最小输入 | 路径、ID、argv 和选项均有类型、长度、数量与作用域限制 | 参数校验失败时不进入执行器 |
| 最小输出 | 输出裁剪并优先产生结构化摘要、句柄或 Artifact;敏感字段先脱敏 | 截断和脱敏状态随结果返回 |
| 可重试语义 | 改变状态的动作使用幂等键或 revision/CAS,避免模型重试造成重复副作用 | 冲突、重复和授权挑战保持可区分 |
backend/app/services/agent_runtime.py · backend/app/services/sandbox_authz.py · backend/app/services/egress_approvals.pyPrompt compiler
系统提示词策略
提示词由多个 system message 分层编译,而不是把所有内容拼成无法审计的单块文本。 核心规则稳定,工作区风格和本次授权上下文动态注入。
身份、真实性、安全、隐私、指令优先级与通用回答行为。
基础风格 + 温和、热情、标题、Emoji、详细度五个离散特征。
普通 Chat 禁止工具;Agent 模式允许使用本次注册的函数。
工具前给简短可见说明,工具后可更新进展,结束时给完整答案。
选中节点、附件、文档选区与最多 8 个已授权 Skill 包。
协议有效的历史消息、必要的上下文压缩,以及当前用户输入。
风格策略
工作区可选择 default、professional、friendly、candid、
efficient、exploratory、quirky 或 cynical。
五个附加特征均使用 -2…2。风格只控制表达方式,不得改变事实、工具权限或用户当前明确格式。
提示词注入防线
文档与网页摘录明确视为不可信参考,不作为系统指令。
只注入当前用户和工作区有权读取、且与本轮相关的内容。
单包最多 8,000 字符、每轮最多 8 个;脚本不会因提示词自动执行。
接近窗口阈值时保留协议有效的最近后缀,旧事务用持久摘要表达。
backend/app/prompts/fragments.py · backend/app/prompts/compiler.py · backend/app/services/chat.pyTool registry
Tool 注册与调用
Tool 是给模型看的 JSON Schema 函数定义与服务端执行器的配对。注册本身无副作用; 只有模型输出 tool call 后才进入参数校验、权限检查和执行。
| 工具族 | 代表能力 | 写入边界 |
|---|---|---|
| 会话与时间 | 检索 Session 片段、读取历史区段、读取当前时区时间 | 只读 |
| 图谱 | 读取目标/能力图谱、候选节点更新、提出图谱变更 | 正式图谱须用户审核 |
| 学习闭环 | 路线读取/重规划、行动计划、Mastery 读取、Evidence 草稿 | Evidence 只写 pending |
| Provider | 列 Provider/模型、能力查询、配置与 Secret 轮换 | 管理写入要求 workspace.manage |
| Web | search_web、parallel_web_research | 仅在 SearchProvider 可用时注册 |
| 内容产物 | 图片生成、可信组件、Magic Card、沙箱文件 Artifact | 不允许任意代码进入宿主 DOM |
| 扩展 | 声明式 Skill、已授权 MCP 工具 | 沿用各自 Grant 与 Invocation 边界 |
调用结果
执行器返回规范化的成功或失败对象,并可附带来源、Artifact、Extension Invocation ID、Sandbox Session ID 等安全元数据。意外异常不会把 SQL、HTTP、Provider 或沙箱实现细节泄露给模型;权威堆栈保留在服务端日志。
{
"status": "completed",
"data": { "…": "domain result" },
"meta": {
"extension_invocation_id": "…",
"source_count": 3
}
}
Instruction & workflow extensions
Skills
LearnGraph 区分两类 Skill。它们的安装、授权和运行语义不同,不能把文件包中的脚本当作自动 Tool。
指令型文件包
- 至少包含
SKILL.md - 启用且具有
alwaysGrant 后按需注入 system context scripts/*不注册为函数工具- 脚本仅可通过 Docker 无网
sandbox-run显式试运行 - 文件按 ContentBlob 内容寻址保存,禁止绝对路径与
..
声明式工作流
- Manifest 类型为
declarative_review或declarative_workflow - 精确声明受审计的内置领域工具与最小权限
- 启用且持久授权后可注册为 Agent function tool
- 不允许 Shell、Python、JavaScript 或用户可执行代码进入宿主进程
- 图谱写入只产生候选修订;Evidence 保持 pending
生命周期
authorization_required→ready_once / enabled→authorization_required
来源、版本、Manifest、权限或内容哈希变化会废止旧 Grant
Skill Market 只提供系统级预缓存卡片;安装后才复制进工作区授权边界。本机 Skill 探测默认关闭, 只适用于前后端同机环境。翻译是按 content hash、locale 与 model 缓存的查看层,不修改源文件。
backend/docs/mcp-skills.md · backend/app/services/mcp_skills.py · backend/app/services/skill_package.pyModel Context Protocol
MCP
MCP Server 是工作区级外部工具目录。运行时不会信任注册表中的旧描述: 每次调用前重新发现能力并保存不可变 CapabilitySnapshot。
Transport 与网络边界
streamable_http真实执行 JSON-RPC 初始化、工具发现与调用;公网只允许 HTTPS,本机 HTTP 仅限 loopback。
stdio当前没有隔离命令运行器,明确返回 available=false;不会在宿主启动任意命令。
Bearer静态 Token 只保存为加密 Secret 引用,API 响应只返回掩码。
OAuth授权码(PKCE)、动态客户端注册与刷新令牌流程已提供 API 端点;令牌只以加密引用保存,API 只返回掩码投影。
重授权规则
服务身份、工具、资源、Prompt、Schema 或注解变化会改变快照哈希;来源、版本、端点、认证指纹、
Manifest、权限或运行边界变化会改变授权哈希。任一变化都使旧 Grant 失效。
allow_once 在调用前消费,即使远程执行失败也不会恢复。
只有当前快照 enabled 且具有明确持久 always Grant 的 MCP 工具才进入 Agent Tool Registry。
Isolated execution
沙箱
沙箱是 Docker-only、默认断网、按「聊天会话 × 属主用户」隔离的代码与文件执行边界。
宿主机永远不是 fallback:Docker、镜像 digest 或配置缺失时 probe 返回明确 unavailable,不会退化执行。
自 sandboxd 控制面(独立进程/服务)起,LearnGraph 业务进程不再直接控制 Docker Engine:
它只消费版本化、认证的 Sandbox API(create/exec/file/lifecycle),Docker socket、named volume 与
per-sandbox 内部 egress 网络均由 sandboxd 独占管理;app 生产镜像不再包含 Docker SDK。
系统内有两条互相独立的执行路径——sandbox-policy-v1 固定任务(只能跑内置 runner 检查已上传文件)与
sandbox-agent-v1 Agent 工作区(模型写文件、跑 .py/.js 脚本)——共用同一张
SandboxSession 表与后端,但按 policy_revision 严格互斥。
容器硬化基线
network_mode=none、根文件系统只读、/tmp 为 noexec/nosuid tmpfs(64 MiB);容器以 sleep infinity 常驻,命令经 exec 注入。
非 root(uid/gid 65532)、cap_drop=ALL、no-new-privileges、默认拒绝的 seccomp 白名单(显式省略 io_uring)。
镜像必须以 @sha256: 固定;未 pin 时 probe 直接不可用。基础镜像同样按 digest 固定,Bootstrap 冒烟通过后才原子落盘。
Python 强制 -I -B 隔离模式;PIP_NO_INDEX=1、代理变量置空;前端工具链预装在镜像 /node_modules,零下载、不占工作区配额。
生命周期与双 TTL
CREATED → COLD → STARTING → RUNNING → WARM_IDLE → (COLD | EXPIRED)
# STARTING 先落库占位再调 Docker,容量统计含 STARTING/RUNNING/WARM_IDLE
# 超时 kill:容器被丢弃回 COLD;工作区数据保留
# 清理调度器对 STARTING/RUNNING 会话不动 bind mount(看门狗保护)
| 维度 | 闲置 TTL | 绝对 TTL | 到期动作 |
|---|---|---|---|
| 容器(runtime) | 180s | 1800s | 删除容器,会话降级 COLD(cool-down,工作区保留) |
| 工作区(workspace) | 1800s(活动可外推) | 86400s(创建即定死,不可外推) | 删容器 + 二次逃逸校验后 rmtree,会话 EXPIRED |
资源护栏
磁盘配额是双机制:内核级 fsize ulimit 限单文件 + 宿主侧聚合统计在写入前后与执行轮询每一拍检查。
宿主层另有保护:单用户 2 个活跃容器、部署级 20 个、宿主内存分配 ≤70%、CPU ≤80%、保留磁盘 ≥20 GiB、
单用户留存工作区 ≤10 个。默认值见 backend/app/core/config.py(LEARNGRAPH_SANDBOX_*)。
一次 Agent 命令的执行管线
- 01argv 策略
解释器白名单(python/node),禁止
-c/-m/-e/--eval/-p/-i内联代码;入口必须是工作区内相对.py/.js文件;cwd 只能是.;参数数与单参字节受限。 - 02破坏性删除(默认免审批)
rm / dd / mkfs / Remove-Item等命令仅允许work/子树(绝对 /../ 盘符路径直接硬拦);默认LEARNGRAPH_SANDBOX_DELETE_APPROVAL_MODE=off直接放行并保留审计(沙箱隔离,不影响宿主机文件),设为on时恢复会话级单次 Grant(TTL 60s–24h),缺失则 403 授权挑战经 SSE 弹窗回传。 - 03幂等与审计
Idempotency-Key去重;持久化SandboxAgentCommand只存 argv 摘要与脱敏副本,stdout/stderr 落库前同样脱敏。 - 04容量与容器
进程锁 + 跨进程文件锁下校验用户/宿主容量,先提交 STARTING 占位再
docker create;失败回滚 COLD。 - 05快照 → 执行 → 恢复
执行前对工作区做 inode 级快照;100ms 粒度轮询超时与聚合配额;执行后凡未授权的删除一律从快照恢复并抛授权挑战——argv 拦不住的
shutil.rmtree类软删除由此兜底。 - 06结果与 SSE
exit code / 超时 / 截断 / 延迟落库;结果转成
sandbox_status/sandbox_artifactMessagePart 流式呈现。
双层存储:物理工作区 + 逻辑工作区
宿主 {root}/{user}/{session} 挂载为容器 /workspace;随沙箱会话过期被清理。
SessionWorkspaceEntry + ContentBlob(sha256 去重、引用计数)随聊天会话存续。写入双写两层、读取优先逻辑层——Docker 不可用时附件读取与文件列举依然可用。
Agent 工具面
六个工具(sandbox_write_file / read_file / list_files / exec / transcribe_audio / publish_file)
仅在 workspace.manage 权限且 SANDBOX_AGENT_ENABLED 时注入;chat_session_id
由服务端注入,模型不可伪造。Agent 模式开流前先过 agent_sandbox_readiness() 就绪契约,
不可用则整轮拒绝并给出中文修复步骤。音频转写是宿主侧桥:容器不联网、不接触凭据,ASR 调用在宿主完成计费与执行后回写工作区。
API 面
| 端点组 | 权限 | 职责 |
|---|---|---|
/sandbox/profiles · /bootstrap* · /agent/readiness | 成员 / workspace.write | 能力探测、一键镜像构建(进度/日志)、Agent 就绪契约 |
/sandbox/agent/sessions · /agent/commands · /agent/files/* | workspace.manage | Agent 会话、命令执行(幂等)、文件读写列举 |
/sandbox/tasks* · /sessions* | 成员 | 固定 runner 任务(file_inspect / extract_inert_text)、会话查询与手动清理 |
/sandbox/workspace/* · /authorizations* | workspace.manage / workspace.write | 逻辑工作区条目与发布;破坏性删除 Grant 的创建与吊销 |
seccomp 相对 Docker 默认档有意放行 clone/unshare/chroot 以支持 Chromium 的
user-namespace 沙箱;删除 Grant 在 TTL 窗口内可重复使用(非一次性);
runtime_kind 的两个取值已解析到同一镜像(python-node-browser 已标记废弃);
network_policy.allowed_hosts、任务队列(SANDBOX_QUEUED_TASKS_PER_USER)与
SANDBOX_TASK_TTL_SECONDS 为占位,当前执行全部同步阻塞。
沙箱验收依赖需要真实 Docker 的 backend/scripts/verify_sandbox_runtime.py 端到端脚本。
本节指 Docker 执行沙箱;前端另有 iframe 生成式组件预览(sandbox-artifact.tsx,严格 CSP、connect-src 'none'),以及 MessagePart.type="sandbox" 这一混合渲染类型。三者互不相同,勿混用。
backend/app/services/sandbox.py · backend/app/services/sandbox_authz.py · backend/app/services/session_workspace.py · backend/app/providers/remote/sandbox.py · backend/sandbox/Egress approval chain
沙箱联网与审批:默认断网,出网必须过审
沙箱没有直接出网。需要联网的能力(网页抓取、依赖下载)必须经过工作区级审批链;
没有合法策略时容器保持 network_mode="none"(fail-closed,默认拒绝网络是安全基线)。
按工作区评审的出网策略:host 域名 + HTTPS 端口 + 有效期 + 审批记录 + 不可变 digest;拒绝通配符、拒绝 IP 字面量。抓取白名单(web_fetch.policy)与通用 Agent egress 策略各自独立,抓取授权不会扩大通用出网面。
SandboxEgressProxy 是纯 asyncio CONNECT 代理。每次 CONNECT 都按策略 digest 鉴权(X-LearnGraph-Policy-Digest 头自证身份),并在连接时重新解析 DNS + 逐地址分类——私网 / 环回 / 链路本地 / 元数据 / 保留段全拒,防 DNS-rebinding(审批时是公网、连接时变内网)。
有合法策略 → 容器加入 learngraph-egress 网络,注入 HTTP_PROXY/HTTPS_PROXY 与 LEARNGRAPH_EGRESS_POLICY_DIGEST;无策略 → 完全离线。拒绝时返回可审计原因。
Egress 审批链默认开启(D2.1 全量落地),审批记录可查询、可吊销;服务端重启可恢复未完成审批流。
外部来源可信获取:联网下载,离线执行
download_external_image 与 download_github_source 不会给沙箱临时联网,也不向模型暴露
curl / wget / git clone。宿主侧 ExternalAcquisitionService 复用 Egress 审批卡 UI,但使用独立的
external_acquisition capability:下载批准只放行宿主下载器,绝不派生沙箱出网策略,通用沙箱 Grant 也不会自动放行下载。
allow_once 额外绑定规范化资源请求的 SHA-256,并在联网前通过条件更新原子认领(并发只允许一次执行)。
每次连接都重新解析全部 DNS 答案并拒绝任何私网/环回/元数据地址,HTTPS 连接固定到已分类的公网 IP;跨主机重定向必须再次审批。
限制 wire bytes、像素与帧数,只接受 PNG/JPEG/WebP;完整解码后重新编码,去除 EXIF/ICC 等非必要元数据,再写入内容寻址存储与会话 inputs/。`search_images` 之后可传 `urls` 数组(2–8 张)并行下载,单张失败不中断整批。
通过 API 将 ref 固定为 40 位 commit,检查 tree 截断、文件数/大小、Git mode,拒绝 symlink、submodule 与 Git LFS pointer;全部文件下载验证完成后再写入工作区,并生成逐文件哈希 manifest。
ExternalAcquisitionReceipt 记录请求摘要、requested/final URL、重定向链、解析地址、声明/检测 MIME、wire/stored bytes、文件或 manifest SHA-256、GitHub commit 与审批请求 ID;ExternalAcquisitionFile 逐文件关联 receipt 与 FileRecord。
内容先进入 ContentBlob / FileRecord / SessionWorkspaceEntry,再由现有 Agent Workspace 按需 materialize;Docker 不可用或容器销毁也不丢失下载结果和来源。单次下载的 blob、条目、FileRecord、receipt 与关联行在同一事务提交,失败不留半份仓库。
web_fetch 热容器池
沙箱隔离的网页抓取每次请求原本要付完整容器生命周期(create → materialize → run → delete ≈ 1.5-3s 宿主开销/次)。
进程级热容器池按 工作区 + 白名单 + settings 指纹 复用空闲容器:
| 机制 | 行为 |
|---|---|
| 池 key | 工作区 + 域名白名单 + settings 指纹(镜像/内存/CPU/workspace 路径…);配置变化即换池,旧池按 idle TTL 自然淘汰 |
| 并发与饱和 | 默认池大小 4(0 关闭池回退旧行为);饱和时等待预算内排队,超时抛 FetchPoolSaturated |
| 驱逐 | 任何超时 / 截断 / 后端错误的容器立即销毁,绝不回流复用 |
| 回收 | 空闲 600s 由惰性 prune + 调度器 sweep 回收;进程退出 atexit 全量关闭 |
预构建镜像与真实进度
本地构建分钟级,支持发布预构建镜像到阿里云 ACR:只持久化 RepoDigest(本地 image ID 不可移植),
docker-py pull 显式传完整 tag(丢 tag 会静默拉 :latest 404),auto 模式拉取失败回退本地构建、显式 prebuilt 仍 fail-closed。
构建进度百分比来自后端真实 docker 事件(下载/解压/构建阶段 40-68%),BuildKit 的 \r/ANSI 日志清洗为可读行,
进度持久化、只前进不倒退、刷新不回跳;前端只渲染、不模拟。
网页抓取的白名单、审批记录与沙箱执行权限是独立维度;抓取授权只影响抓取路径,不会放大 Agent 通用出网面。
backend/app/services/sandbox_egress_proxy.py · backend/app/services/sandbox_network_policy.py · backend/app/services/egress_approvals.py · backend/app/services/external_acquisition.py · backend/app/services/session_workspace.py · backend/app/providers/sandbox_fetch_pool.py · backend/app/services/sandbox_bootstrap.pyHost service access
宿主服务访问:Bridge 默认,可信桌面可直连
整体 Docker 部署后,容器内 127.0.0.1 是容器自己,Provider/MCP 配置里保存的
http://127.0.0.1:11434(Ollama)这类逻辑地址无法直达真机。LearnGraph 不做
if docker: host.docker.internal 补丁,而是由Host Service Resolver在装配点
(backend/app/providers/factory.py 的 effective_base_url 与探测路径)按部署形态统一改写,
业务层与数据库始终保存逻辑地址。改写策略由 LEARNGRAPH_HOST_ACCESS_MODE 决定:
auto(默认)| bridge | direct | off。
真机常驻授权反向代理(node scripts/host-bridge.mjs,端口 34115):只转发注册表显式放行的服务(默认 DENY、目标默认仅 loopback、逐请求 Bearer token),每次允许/拒绝写 JSONL 审计。Resolver 把 loopback 地址改写为 {bridge}/services/{id}/...;compose 已内置 extra_hosts: host.docker.internal:host-gateway 并挂载 token 文件,容器侧零配置。
LEARNGRAPH_HOST_ACCESS_MODE=direct:容器内 loopback 的 Provider/MCP 地址改写为 host.docker.internal:同端口(端口/路径/query 原样保留),Docker Desktop 把该别名转发到真机同端口。无注册、无 token、无审计——主应用可触达宿主全部本地 TCP 端口,仅建议 Windows/macOS Docker Desktop 个人可信环境;非容器(源码安装)自动回退为不改写。
off 关闭全部宿主改写(loopback 保持字面量);源码模式(npm run dev,personal_desktop)默认直连、零影响。MCP 适配器在 direct 模式下把 host.docker.internal 加入明文 HTTP 白名单(allow_private_hosts),并跳过 bridge token 头。
改写规则(host_service_resolver)
| 条件 | 结果 |
|---|---|
host_access_mode="direct" 且 host 为 loopback | → http://host.docker.internal:同端口/路径?query |
host_access_mode="off" | URL 原样返回 |
| bridge 模式且 host 为 loopback | → {bridge}/services/{id}/{path}?query(ollama 与 ollama_embedding 共用注册 id ollama) |
| 非 loopback(远端 API) | 永不改写 |
| personal_desktop / 未配置 bridge 地址 | URL 原样返回(源码直连) |
LEARNGRAPH_HOST_ACCESS_MODE=direct
docker compose up -d --build
状态与提示
GET /api/v1/providers/host-bridge/status 返回 host_access_mode、
bridge_reachable(bridge 可达性)、host_gateway_reachable(direct 模式下
host.docker.internal 是否可解析)、bridge_token_ready 与现成的中文 guidance;
Provider 管理页按模式展示:bridge 未运行 → 提示启动 node scripts/host-bridge.mjs 并重启容器;
direct 网关不可达 → 提示核对 extra_hosts: host.docker.internal:host-gateway。
direct 只在主应用 Resolver 装配点生效;动态沙箱、插件与不可信执行容器内的 localhost 永远只表示容器自己。原生 Linux 下 host-gateway 只到 Docker 网桥,宿主服务需监听网关可达接口(可能扩大局域网暴露面),否则请继续使用 bridge。
backend/app/core/config.py(HostAccessMode)· backend/app/providers/host_service_resolver.py · backend/app/services/host_service_bridge.py · backend/app/services/management.py(host_bridge_status)· docker-compose.yml · frontend/src/features/settings/provider-pages.tsxAtomic memory
记忆系统:原子记忆
记忆不是"把完整历史塞 prompt"。LearnGraph 的记忆系统以原子记忆为单位: 自动沉淀对话与任务中值得长期记住的事实,并在后续会话中按相关性回填给 Agent。 本节只描述当前提供的能力;底层架构仍在演进中。
对话与任务结束后自动识别值得长期记住的事实(用户偏好、项目决策、纠错反馈等),无需手工录入;由工作区"记忆"总开关控制。
每条记忆独立成条,记录来源与置信度;同一事实的多次修正更新原条目,而不是堆积重复内容。
后续会话只装配与当前任务相关的记忆,不注入完整历史;受预算约束,避免上下文失控。
用户可查看、修正或删除记忆;用户明确给出的纠正优先于自动提取的结果。
记忆按任务 / 工作区 / 用户分级,跨工作区访问受授权约束。
记忆带时效信息,过期或失去依据的内容自动降级或失效,避免误导后续会话。
Artifact gateway
产物与分享:不可变版本、分享令牌与卡片索引
沙箱、Agent 和可信组件产生的产物(sandbox_artifact、组件 Artifact、子应用 bundle)统一经过 Artifact Gateway 管理,遵循"发布即冻结"的版本模型,而不是工作区临时文件。
发布时生成不可变版本(版本 + 内容 hash 绑定),同一版本不可替换;预览与分享都引用版本而非工作区路径,工作区过期不影响已发布产物。
分享生成令牌(token),只读公开访问;令牌可撤销,撤销后原链接立即失效。
删除为软删除(保留审计痕迹);删除产物时级联撤销其分享令牌,避免"删了还公开着"。
产物自动聚合为会话卡片(卡片网格),按会话/会话类型索引,可在产物页统一管理(编辑、分享、删除)。
组件 Artifact 由服务器持有模板渲染(CSP 锁死,见可信组件节);离线沙箱渲染可用时附加有界截图,不可用时 runtime_status=unavailable 安全降级。
交互式子应用支持 bundle 预览地址(独立 preview 源),发布/预览到期校验做时区归一化。
backend/app/services/artifact_gateway.py · artifact_cards.py · canvas_cards.py · component_renderer.py · backend/app/api/routers/artifact_gateway.pyStreaming contract
消息与 SSE
SSE 是传输层,不是事实源。Session、Message、MessageVersion、MessagePart 与流事件均持久化, 从而支持刷新读取、断点续传、分支、版本比较和 Provider 协议续接。
前端以 Message Part 渲染文本、来源、推理摘要、工具状态、可信组件和沙箱 Artifact。 未知组件安全降级为数据预览,不注入任意 HTML、脚本或 React 代码。
单写者下的落库工程
SQLite 只有一个写者,如果每个流 delta 都 commit,token 投递会被串行化在 fsync/锁获取后面。策略:
started / completed / failed / checkpoint 等锚点事件立即落库;普通 delta 按条数或时间节奏批量落库——可靠性与吞吐兼得。
自动标题、建议、活动摘要等每次调用都花钱的 LLM 调用按 Session 加锁串行,并发请求先释放快照再 CAS 读,防重复付费。
502/503/504 为中继失败、500 flaky、408/429 明确可重试、529 overloaded;退避用 uniform ±ratio 抖动 + 上限,避免惊群。
Agent 工具调用用模块级共享单 worker executor + 宿主超时;超时后链路继续,挂起线程不阻塞回合。
心跳、断线与恢复
工具执行 / 退避 / 搜索期间定期发心跳,防止中间代理断流,也防止用户误以为卡死。
后端重启把未完成流标为 interrupted(可恢复)或 failed(可重试);interrupted 保留重放窗口。
retry 走独立端点重新生成消息版本;生成器必须 yield from 流(曾因 return stream() 把生成器当 StopIteration 值丢弃导致只发 ready 就 EOF)。
页面关闭后任务继续并持久化;同账号多浏览器可恢复获取;关页不再等于白烧钱。
backend/app/services/chat.py · backend/app/core/process_lock.py · frontend/src/lib/session-streams.tsTrusted components
可信组件
可信组件是助手消息流里的声明式 UI 卡片(选择题、填空、天气卡、指标卡、图片框等)。它用 JSON Schema
声明数据面,而不是把任意 HTML/React 注入宿主 DOM。Agent 可通过 canvas_emit_trusted_component
发布内置组件,也可注册并授权第三方组件 Manifest 再发布。所有写操作经后端 Schema 守卫、哈希与审计,
前端对未注册或未通过 Schema 的组件安全降级为 JSON 预览,绝不执行脚本。
内置目录 · 宿主 DOM
- 8 个内置
component_type:weather_card、metric_card、option_group、single_choice、multiple_choice、fill_blank、short_answer_table、image_frame - 渲染器
trusted-bundle,由前端编译好的 React 组件在宿主 DOM 直接渲染 - 端到端可用,是 Agent 默认的表单 / 展示通道
- 权限由系统白名单固定,工作区不可改写
第三方 Manifest · 隔离浏览器
- 任意
component_id,经工作区导入 Manifest 注册 - 渲染器固定为
sandbox,结果以sandbox_artifact交付 - 条件可用:依赖未配置的隔离浏览器渲染器,缺失时
runtime_status=unavailable安全降级 - 需工作区显式授权后才可发布或生成 Artifact
协议生命周期
register→health_check→authorize(+enable)→artifact / event_validate
版本、Schema、权限或包哈希变化会废止旧授权,需重新授权;同一 version 的 Manifest 不可替换。
- 01register
提交完整 Manifest,服务端做静态 Schema 守卫、签名记录与 example_data 校验,落 Plugin 与 ManifestVersion。
- 02health_check
对当前 Manifest 跑 JSON Schema 校验(render 检查在隔离浏览器未配置时标记 unavailable,不阻断)。
- 03authorize + enable
工作区对当前版本 + 权限指纹授权并启用插件;内置组件由系统固定,工作区不可改。
- 04artifact / event_validate
用授权后的组件把
data校验成 Artifact,或把用户事件按event_schema校验后再处理。发布前由assert_can_enable校验 Manifest、授权与 health 全部当前。
Manifest 字段规范
component_id全局唯一标识,^[A-Za-z0-9][A-Za-z0-9._-]{1,119}$;内置 8 个 ID 与 source=builtin 保留,第三方不可占用。
versionSemVer,max 40 字符;同一 version 落库后不可替换。
display_name展示名,1–160 字符。
renderer第三方导入固定为 sandbox;trusted-bundle 仅系统内置可用。
source来源标签,1–160 字符;builtin 为系统保留值。
package_hash组件包 sha256(64 位十六进制),由发布方计算并声明;服务端标记 declared_unverified。
signature可选签名声明(ed25519 或 ecdsa-p256-sha256);当前无信任锚,仅记录为 unverified。
data_schema组件数据的 JSON Schema,必须顶层 type=object 且 additionalProperties=false。
event_schema用户事件 JSON Schema,同样要求顶层闭合对象。
permissionsnetwork_domains(精确 DNS 主机,禁通配/带 scheme)、file_read、clipboard_write、message_actions(小写标识)。
size_limitsmin_height ≥40、max_height ≤2000,且 min ≤ max。
example_data必须通过自身 data_schema 校验的最小示例。
skill_triggers最多 64 条触发词,可选。
Schema 安全约束
data_schema / event_schema 顶层必须 type=object 且 additionalProperties=false。
字段名不得为 html、raw_html、javascript、script、srcdoc、react_code、dangerouslysetinnerhtml;实例字符串不得含 <script、javascript:、<iframe、srcdoc= 等标记。
Schema 不得用非 #/ 开头的 $ref;不得声明 contentMediaType 为 text/html、application/javascript、text/javascript。
Schema 不得含 pattern 字段(不执行调用方提供的正则)。
单 Schema ≤64KB、单实例 ≤64KB、深度 ≤16、节点 ≤1000。
可选字段省略即可,不要传 null;后端对内置 props 会做 null 剥离。
内置组件最小合法 props
{
"title": "你更希望怎样验收这次学习?",
"description": "单选一项后继续",
"options": [
{ "id": "project", "label": "完成一个小项目" },
{ "id": "explain", "label": "能够清楚讲解" }
],
"allow_custom": true,
"allow_skip": true,
"submit_label": "确认并继续"
}
{
"title": "请补全 ACID 中的 A",
"prompt": "ACID 中的 A 代表 ____",
"placeholder": "Atomicity / 原子性",
"multiline": false,
"submit_label": "提交",
"blank_ids": ["answer"]
}
{
"title": "简答题",
"columns": ["问题", "你的回答"],
"rows": [["为什么需要索引?", ""]]
}
{
"title": "杭州明日天气",
"location": "杭州",
"condition": "多云",
"temperature_c": 27,
"high_c": 29,
"low_c": 21,
"summary": "适合户外轻量复习",
"unit": "C",
"actions": [
{ "id": "create_plan", "label": "生成明日学习计划", "event": "create_plan" }
]
}
{
"title": "今日学习指标",
"description": "来自当前目标进度",
"metrics": [
{ "id": "mastery", "label": "掌握度", "value": "62%", "hint": "近 7 日" },
{ "id": "reviews", "label": "待复习", "value": 3 }
]
}
{
"title": "示意图",
"alt": "B+ 树结构示意",
"status": "placeholder"
}
完整第三方 Manifest 示例
以下是一个自定义 quiz_card 的完整 Manifest,可直接用于 POST /api/v1/plugins/components
或智能体的 component_register_manifest 工具。它满足全部 Schema 安全约束:顶层闭合对象、无禁用字段、
无 pattern / 外部 $ref、example_data 能通过自身 data_schema。
{
"component_id": "acme.quiz_card",
"version": "1.0.0",
"display_name": "Quiz Card",
"renderer": "sandbox",
"author": "Acme",
"source": "acme-marketplace",
"package_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"compatible_learngraph": { "minimum": "0.1.0" },
"uninstall_behavior": "retain_data",
"data_schema": {
"type": "object",
"additionalProperties": false,
"required": ["question", "answer"],
"properties": {
"title": { "type": "string", "maxLength": 500 },
"question": { "type": "string", "maxLength": 2000 },
"answer": { "type": "string", "maxLength": 2000 },
"hint": { "type": "string", "maxLength": 500 }
}
},
"event_schema": {
"type": "object",
"additionalProperties": false,
"required": ["type", "value"],
"properties": {
"type": { "type": "string", "enum": ["submit", "change", "select"] },
"value": {
"oneOf": [
{ "type": "string", "maxLength": 10000 },
{ "type": "array", "maxItems": 100, "items": { "type": "string", "maxLength": 1000 } }
]
}
}
},
"permissions": {
"network_domains": [],
"file_read": false,
"clipboard_write": false,
"message_actions": ["submit"]
},
"size_limits": { "min_height": 80, "max_height": 480 },
"skill_triggers": [],
"example_data": {
"title": "练习",
"question": "2 + 2 = ?",
"answer": "4",
"hint": "十以内加法"
}
}
事件协议
用户在卡片上的交互以事件形式回传,按组件 Manifest 的 event_schema 校验。内置组件的默认
event_schema 要求 type(submit / change / select)
与 value(字符串或字符串数组)。allowed_events 限定可触发的事件名,
选项类默认 ["submit"],weather_card/metric_card 由
actions[].event 推导,image_frame 默认为空。
智能体创建并应用
Agent 在拥有 workspace.manage 权限时,可直接在对话里创建并应用可信组件,无需切到设置页:
- 01component_register_manifest
提交第三方 Manifest,服务端做静态与健康检查并落审计;内置 ID 被拒绝。
- 02component_authorize
对刚注册的
plugin_id+manifest_version_id授权当前工作区并启用插件(enable=false可仅授权不启用)。 - 03component_list
查询已注册组件及其授权/启用状态,决定下一步是授权还是发布。
- 04canvas_emit_trusted_component
对自定义
component_type传其component_id发布;已授权则按sandbox_artifact交付。
第三方组件渲染器固定为 sandbox,依赖隔离浏览器渲染器;当前未配置,发布会以
sandbox_artifact 安全降级(runtime_status=unavailable)交付,不会把任意代码注入宿主 DOM。
内置 8 类组件不受此限制,端到端可用。
API 面
| 端点组 | 权限 | 职责 |
|---|---|---|
POST /plugins/components | workspace.write | 导入并校验第三方 Manifest(注册) |
GET /{plugin_id}/manifests · /authorizations · /checks | workspace.write | 列出版本、授权与检查记录 |
POST /{plugin_id}/authorizations · /authorizations/revoke | workspace.write | 授权或撤销当前工作区 |
POST /{plugin_id}/checks | workspace.write | 触发 health / render 检查 |
POST /{plugin_id}/artifacts | 已授权 | 按授权边界生成受控 Artifact |
POST /{plugin_id}/events/validate | 已授权 | 按 event_schema 校验用户事件 |
component_register_manifest / component_authorize / component_list
复用 ComponentService,与上方 HTTP 端点共享 Schema 守卫、哈希、审计与重授权逻辑;
Agent 工具额外要求 workspace.manage。
backend/app/domain/schemas/components.py · backend/app/services/components.py · backend/app/services/canvas_cards.py · frontend/src/components/chat/trusted-component-renderer.tsx · backend/app/skills/canvas_emit_trusted_component/SKILL.mdInteractive sub-apps
交互式子应用:有状态的双向数据通道
交互式子应用(subapp)是可信组件的进阶形态:不只展示,还能收集用户交互数据并接收 AI 写回的状态,
形成"用户操作 → 事件 → Agent 处理 → 页面更新"的多轮对话式交互。UI 与数据契约分离——
interaction_contract 定义数据格式,"千万种交互形式,结果一致"。
协议与生命周期
- 01注册 bundle
subapp_bundles带interaction_contract与component_manifest_id;契约校验顺序与组件 ID 匹配在服务端强制。 - 02发布 / 预览
发布产生不可变版本与 bundle 预览地址(独立 preview 源);发布/预览到期校验做时区归一化。
- 03双向通道
iframe →
postMessage数据出口(宿主桥)→ 事件入库(T1.4 只读观察)→ 事件驱动 Agent 处理 →patch_state写回(T2.5)→ 页面更新。 - 04媒体注入
视频 / 音频经 blob 注入(零联网);地图等外部资源走经审批的会话级媒体域名。
安全不变量
预览 HTML 是固定模板,组件数据是 schema 校验后的惰性 JSON,渲染进 HTML-escaped <pre>;组件提供的 HTML/JS/CSS/URL 永不嵌入("没有可执行面")。
default-src 'none'; script-src 'none'; connect-src 'none';iframe 读不到 cookie/localStorage,发不出 HTTP。
CSP 拦不住 postMessage,因此数据出口只有宿主桥一条受控通道,按 interaction contract 校验、可审计。
组件渲染可选在离线 Docker 沙箱做校验 + 有界截图;不可用则 runtime_status=unavailable 降级,不阻断主流程。
路径是 静态 HTML 预览 → 魔术卡片(可交互控件)→ 双向交互子应用。能力每前进一步,安全边界(模板、CSP、出口)同步验证一层。
backend/app/services/subapp_bundles.py · subapp_event_agent.py · component_renderer.py · backend/app/api/routers/subapps.py · frontend/src/lib/subapp-bridge.ts · frontend/src/lib/trusted-renderer.tsSecurity model
安全与授权
安全边界贯穿路由、服务、扩展授权、Provider Secret、沙箱和输出渲染,而不是单独依赖前端隐藏按钮。
所有资源查询由后端重新校验 Membership、Permission 与 ACL;跨工作区 ID 返回不可枚举错误。
Provider Key 由后端加密保存,不进入浏览器、日志、SSE、审计或导出。
MCP/Skill 的权限集合必须与服务端推导的最小集合完全一致,少一项或多一项都拒绝。
正式目标图谱的重要变更、删除影响与高风险执行保留用户确认。
远程 Provider、Docker 或 Transport 失败时保持失败事实,不落回 mock、宿主执行或硬编码结果。
授权、拒绝、撤销、失败、超时、大小阻断和成功调用均记录工作区审计。
认证、供应链与发布治理
首次登录强制改密(独立页面 + 刷新恢复)、认证限流防灌水;按端点明确权限边界、避免尾斜杠绕过。Demo 默认是禁用的普通用户,旧版管理员标记会在启动时移除;Docker 部署只能在宿主机根目录 .env 显式设置 LEARNGRAPH_ENABLE_DEMO_LOGIN=true 并重建应用容器后启用(仅开发/本地环境有效)。前端不内置账号密码,入口由后端配置控制;关闭后普通登录、快捷登录与已有会话均失效。演示数据仍由 LEARNGRAPH_ENABLE_DEMO_SEED 独立控制。
沙箱镜像 lockfile + 供应链清单校验、镜像只持久化 RepoDigest、MCP 凭据接入版本化密钥存储加密、工具输出脱敏(URL 凭据/密钥赋值)。
沙箱 inode / 文件数 / 目录数 / 快照保留限额;浏览器预览 Range 截断;宿主内存/CPU/磁盘保留水位。
上线前全盘审计(生产构建、登录并发、SSE 心跳、越权、限流、关页计费、首屏体积)→ 分级修复 → 回归验证 → 按"个人/小团队、单实例单进程"边界放行(CONDITIONAL_GO)。
安全是默认值而不是功能:沙箱无策略即离线(fail-closed)、工具未授权即拒绝、Provider 能力不匹配即显式 unavailable—— 任何"静默降级"都被视为缺陷。
Repository map
代码地图
frontend/src/main.tsxReact 入口frontend/src/App.tsx页面路由总表frontend/src/api/client.ts认证、工作区头、JSON/SSE 客户端frontend/src/features/按业务领域组织页面与交互backend/app/main.pyFastAPI 生命周期与调度器backend/app/api/routers/HTTP/SSE 契约与依赖边界backend/app/services/chat.pyChat、上下文、Agent 循环与持久化backend/app/services/agent_runtime.py工具定义、注册与执行分派backend/app/services/mcp_skills.pyMCP/Skill 生命周期、Grant 与 Invocationbackend/app/services/memory_*.py原子记忆:提取 / 存储 / 检索 / 回填 / 纠错 / 时效管理backend/app/services/sandbox.py沙箱任务与 Agent Workspacebackend/app/services/sandbox_egress_proxy.py出网 CONNECT 代理(策略 digest + 地址重分类)backend/app/services/egress_approvals.pyEgress 审批链与策略落盘backend/app/providers/sandbox_fetch_pool.pyweb_fetch 热容器池(复用 + 并发)backend/app/services/artifact_gateway.py产物不可变版本与分享令牌backend/app/services/component_renderer.py服务器持有模板 + 离线沙箱渲染降级backend/app/services/subapp_event_agent.py交互子应用事件驱动 Agent(observe / patch_state)backend/app/prompts/系统提示词片段与风格编译器backend/app/repositories/工作区作用域数据访问backend/app/providers/ports/外部能力契约backend/app/providers/local|remote/适配器实现backend/app/domain/models.py核心持久化模型backend/app/domain/schemas/API 输入输出 Schemamobile/native/手机 APK(原生 Compose + 内嵌网页版,v0.12.0)mobile/native/…/util/Haptics.ktA类触觉:回复/agent/达成 震感 + 提示音mobile/native/…/util/TtsSynth.kt文本朗读(B4 任务结果/耳机)mobile/native/…/widget/…桌面小组件(B3 入口版)mobile/native/…/notify/ReplyNotifier.ktB1 agent摘要/B2 复习提醒 轮询进通知mobile/native/…/data/ShareInbox.kt分享收件箱(Share Target 本地暂存)mobile/native/…/ShareReceiverActivity.ktACTION_SEND 接收(文本/图片)mobile/native/…/QrScanActivity.kt扫码连接(CameraX + ML Kit)mobile/native/…/util/PhotoCapture.kt拍照即问(系统相机 → data URL 回调)mobile/native/…/util/ShortcutActions.kt长按快捷方式(新对话/记笔记/任务/切换服务器)mobile/native/…/ui/web/WebAppScreen.kt网页版桥(LearnGraphNative)扩展 + 断线横幅/重连frontend/src/lib/native-bridge.ts网页版原生桥类型化封装frontend/src/features/mobile/收件箱三选一 / 快捷动作消费 / 连接二维码backend/app/api/routers/chat.pyPOST /sessions/{id}/messages/async 后台生成端点Extension guide
扩展指南
新增 Provider
- 01定义 Port
优先复用现有 Protocol;不要让业务 Service 依赖厂商 SDK。
- 02实现 Adapter
显式建模可用性、超时、响应校验、远程能力和安全诊断。
- 03接入 Catalog / Probe
配置保存后执行真实探测,失败保持 disabled。
- 04记录 Trace / Usage
保存 Provider ID、请求标识、用量与安全错误分类。
- 05真实验收
用部署方明确配置的远程服务验证,缺凭据时标记未完成。
新增 Agent Tool
- 01从领域用例开始
Tool 只调用现有 Service 或受控 Port,不直接访问数据库句柄。
- 02声明严格 Schema
限制字段、长度、枚举与 additionalProperties。
- 03确定授权与副作用
按用户角色、工作区、资源范围与是否需二次确认注册。
- 04规范化结果
裁剪大小、脱敏错误、记录 Invocation / Audit,并返回可追踪来源。
- 05验证完整循环
真实模型发起调用、真实服务执行、刷新后仍可读取结果。
Verification
验证与完成定义
“现有测试通过”不等于业务闭环已完成。重要变更需要同时验证可见结果、后端事实和一个高风险失败/恢复边界。
# 常规检查(不自动包含 E2E 与真实远程 Provider)
npm.cmd run check
# 真实浏览器 E2E
npm.cmd --prefix frontend run test:e2e
# 已配置真实远程模型后
.\scripts\verify-real-provider.ps1
保持来源、权限、事务、审计和用户审核边界一致。