LearnGraph 开发者文档
MVP · 当前代码
Developer platform

理解 LearnGraph
从一条真实调用链开始

LearnGraph 是一个目标驱动、证据驱动的学习图谱智能体。本页描述当前仓库中可验证的架构边界, 以及 Agent、渐进式工具、Skills、MCP、Docker 沙箱和系统提示词如何协同工作。

G Goal 结构化目标
R Representation 可审核图谱
E Evidence 可追溯证据
M Mastery 掌握度判断
A Action 下一步行动
i
本文档的事实边界

“已注册接口”不等于“外部服务已连通”,“支持 Provider”不等于“当前部署已配置”。 文中始终把代码能力、运行配置和真实远程验收分开描述。

01

Reading guide

实现状态与阅读约定

LearnGraph 仍处于早期开发阶段。开发者文档采用三种状态词,防止将未来设计或配置入口误解为已完成能力。

当前实现

可在路由、服务、仓库或 Provider 适配器中追踪到完整调用链,并有持久化模型支撑。

条件可用

代码已接入,但依赖部署配置、外部凭据、Docker 或远程服务;缺失时应显式返回 unavailable。

设计目标

仅存在于总体架构或设计 TODO 中,不应作为当前 API 行为或验收结论。

事实优先级 设计共识→设计 TODO→总体架构→实现 TODO→OpenAPI / 模型 / 代码
02

Quick start

本地启动

根目录脚本负责编排 React/Vite 前端与 FastAPI 后端。默认数据库和对象存储均为本地实现。

PowerShell
# 安装依赖并启动(首次运行)
npm.cmd run dev:install

# 后续联合启动
npm.cmd run dev

# 静态检查、现有测试与生产构建
npm.cmd run check
Webhttp://127.0.0.1:5173
APIhttp://127.0.0.1:8000
OpenAPIhttp://127.0.0.1:8000/docs
Health/api/v1/health
!
默认没有模型回退

未配置模型 Provider 或远程调用失败时,服务必须明确报错。 确定性的本地演示 Provider 仅在 LEARNGRAPH_ENABLE_LOCAL_DEMO_PROVIDER=true 时启用, 不能作为真实验收证据。

03

Architecture

系统架构

当前实现是前后端分离的模块化单体。浏览器只调用 LearnGraph 后端;外部模型、搜索、抓取、研究、 MCP 和存储都由后端经 Port/Adapter 边界访问。

Client
React 19 · TypeScript · Vite Router · TanStack Query · React Flow · Streamdown
JSON / SSE
Gateway
FastAPI · /api/v1 Bearer Session · Workspace Scope · RBAC / ACL · Error Envelope
typed use cases
Domain
Services + Repositories Goal · Graph · Chat · Memory · Evidence · Mastery
Runtime
Agent + Tool Registry Skills · MCP · Sandbox · Provider Gateway
ports
Facts
SQLAlchemy 2 · SQLite 规范业务事实源
Adapters
Local + Remote Providers Filesystem · HTTP APIs · Docker

后端依赖方向

api / routers→services→repositories / domain 或provider ports←local / remote adapters

业务服务不直接依赖厂商 SDK。新增外部能力应先定义或复用 Port,再提供适配器、能力探测和明确失败状态。

来源:backend/app/main.py · backend/app/api/router.py · backend/app/providers/ports/
04A

Architecture evolution

从 v0.1 起步到可治理 Runtime

旧设计文档中的 c173b36 基线来自 rebase 前分支,不在当前 main 历史;当前 main 以 ddf3a1c(v0.1 首发)为起点,HEAD 57fd37f 把记忆系统、沙箱审批制出网、交互式子应用与产物版本化推进到可观察、可恢复、可审计的边界,并已完成一轮上线前全盘审计(NO_GO → 修复 → 有条件放行)。

起点 · ddf3a1c(v0.1)先让能力可靠进入系统

建立首版媒体输入、转录路由、音视频附件策略、解析器、文件服务与验证脚本基线;失败从“泛化错误”变成可定位的能力问题。

07-31 ~ 08-08 · 记忆 WP 系列记忆系统升级

长期记忆从“摘要 + 检索”升级为原子记忆能力(自动提取、按需回填、用户纠错优先),并切换为默认读取链路;事件溯源等底层架构仍在演进中。

08-09 ~ 08-10 · 安全与审计Egress 审批默认开启 + 上线前审计

沙箱 Egress 审批链全量落地并默认开启;上线前审计发现生产构建 P0 白屏、登录并发、越权等问题,逐项修复后按边界放行。

当前 · HEAD让能力可治理地运行

记忆系统、沙箱 readiness 与 egress 授权、原子工具、渐进披露、断线恢复、产物版本化与分享令牌、交互式子应用全部就位。

01能力先于格式

按 MIME、解析能力和 Provider capability 路由;音频、视频、图片和文本文档不共享一条假设链。

02边界先于便利

沙箱、工具、Grant、工作区和 Provider 都是独立边界;没有能力就明确 unavailable,不静默落回宿主。

03事实先于视图

数据库记录、事件、revision 和 audit 是事实;SSE、UI 面板和缓存都是可恢复或可重建的视图。

04按需披露

模型先看到能力摘要和授权范围,再在需要时发现工具、读取大结果或展开上下文,减少噪声与权限面。

05静默降级即缺陷

枚举校验、能力门控、失败边界都要显式:要么显式报错,要么显式兜底,绝不静默改变语义(记忆摘要空值的教训)。

06门禁先于发布

上线前审计(构建、并发、越权、限流、计费)与 CONDTIONAL_GO 边界是发布纪律,不是可选项。

baseline

v0.1 起步版

  • 重点是媒体能否被正确解析和路由
  • 附件策略与 Provider 失败边界开始显式化
  • 验证以脚本和单次请求为主
current

记忆 + 治理版

  • 输入、工具、沙箱和记忆均有稳定事实与生命周期
  • 结果可流式呈现,也可从持久化记录恢复
  • 每个高风险动作都有授权、审计和可拒绝路径
对比起点: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.py
04B

Media pipeline

媒体输入:转录、视觉与文档证据

媒体不是“把文件塞进 prompt”。当前输入管线先判断文件能力,再选择原生模型、转录 Provider、索引检索或外挂视觉描述,并把每一步的状态保留下来。

01识别MIME · size · storage
→
02路由native · transcription · vision
→
03解析caption · transcript · chunks
→
04持久化trace · cache · revision
→
05回答evidence · SSE · retry
音频 / 视频

先验证 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.py

Request gateway

API 网关与请求边界

当前仓库没有独立的网关微服务。FastAPI 的统一 /api/v1 路由层就是应用网关: 它终止认证、解析工作区作用域、校验权限、稳定错误结构,并把请求交给领域服务。

01
ApiClient添加 Bearer、X-Workspace-ID 与请求 ID
02
Router / Schema类型化输入、错误边界、HTTP/SSE 契约
03
Authorization重新校验 Membership、Permission 与资源 ACL
04
Service编排事务、Provider、审计与幂等策略
05
Repository / Port持久化事实或显式调用外部能力

网关职责

身份

正式登录接口签发不透明 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
05

Durable facts

持久化与事实源

SQLite 是 MVP 的规范业务事实源。本地文件系统承载默认对象存储, 但文件路径、外部 UUID 或 SSE 事件都不能替代数据库中的稳定业务身份。

层保存内容关键边界
SQLAlchemy / SQLite账号、RBAC、Goal、Graph、Session、Evidence、Provider、授权、审计当前规范事实源
Message timelineMessage、MessageVersion、MessagePart、SSE 事件、Provider TraceSSE 只负责传输
Memory原子记忆(提取 + 存储 + 按需回填)记忆是长期事实,可随用户纠错更新
Object Storage上传文件、生成产物、内容寻址 Blob默认本地,可经迁移边界切换
i
迁移不是双写

数据库和对象存储切换需要维护锁、预检、校验、切换点与回滚;不能静默双写或降级。

06

Capability adapters

Provider 层

Provider 是外部能力网关,不是业务事实源。业务层依赖 Protocol 定义的 Port; 适配器负责协议差异、探测、超时、限额和 Provider Trace。

ModelOpenAI Responses · OpenAI-compatible Chat · Anthropic Messages · DeepSeek · Qwen DashScope(原生搜索通道)· Ollama · GitHub Copilot · Codex · Ollama Cloud统一为 ProviderChatMessage / ProviderStreamEvent;多角色模型(fast / thinking / agentic)
Search通用搜索 Provider + Qwen 原生联网(enable_source / enable_citation)搜索来源与 [ref_N] 角标仅 DashScope 原生协议支持,按能力快照门控;搜索不可用时显式失败,不生成来源
Fetch沙箱热池抓取 · Crawl4AI HTTP · Firecrawl · Qwen hostedURL 安全校验、超时和响应结构限制;沙箱抓取复用热容器池
Image文生图 / 图生图 / 参考图编辑(gpt-image-2 · qwen-image-edit · qwen_image_search)图片模型独立 lane,拦截误作文本模型
ASRDashScope 转录(实时 + 录音转写)qwen 行兜底路由 · input_audio 直连 · 异步录音识别;沙箱内音频走宿主侧 ASR 桥
StorageLocal filesystem · MinIO通过迁移流程切换
Sandboxsandboxd 控制面app 无 Docker 权限;sandboxd 独占 socket/named volume/内部网络,不可用时明确 unavailable

能力快照与按供应商门控

供应商协议差异(搜索参数、思考模式开关、图片输入、计费字段)被建模为能力快照, 而不是散落在调用点的 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.py
07

Agent runtime

Agent 执行循环

Agent 模式是在持久化 Chat Session 上运行的受控工具循环。模型决定是否发起工具调用, 但工具可见性、授权、执行、结果裁剪、审计和持久化均由 LearnGraph 控制。

Agentmodel turn
1编译上下文system + scope + history
2注册工具capability + grant
3流式推理text / reasoning summary
4执行调用validate + audit
5回填结果tool role message
6完成回答persist + SSE

一次 Agent Turn

  1. 01
    持久化用户输入

    用户消息、附件引用、所选节点与 Session 绑定先成为可追踪事实。

  2. 02
    构建授权上下文

    只加入本次允许访问的节点、文件、选区和 Skill 指令;文档片段被标记为不可信参考数据。

  3. 03
    计算工具注册表

    内置工具按角色开放;声明式 Skill 与 MCP 还必须具有 enabled 状态和持久 always Grant。

  4. 04
    调用模型 Provider

    适配器把结构化消息与 JSON Schema 工具定义发送给已配置远程模型。

  5. 05
    受控执行 Tool Call

    服务端解析参数、重新校验作用域与授权、执行领域服务或 Provider,并保存 Invocation / Audit。

  6. 06
    继续或结束

    工具结果作为 tool 消息回填;模型可继续调用工具,最终文本和所有 MessagePart 一并持久化。

!
Agent 不能直接修改正式目标图谱

lg_graph_propose_change 只创建可审核的 GraphChangeSet 与组件, 不改变已发布图谱;Evidence 由 Agent 写入时保持 pending,不会直接授予 mastery。

09A

Progressive disclosure

先给能力地图,再展开执行细节

渐进式披露不是隐藏能力,而是控制每一轮进入模型上下文的工具数量、描述体量和权限面。运行时先计算本轮能力地图,只把当前模式、用户角色、工作区 Grant、资源作用域和 Provider 状态允许的工具族放入候选集合;模型需要某项能力时,再读取其精确契约或调用按需发现工具。

01Capability map

工具族 · 状态 · 来源 · 授权范围

02Discover contract

按需读取 schema · 限制 · 错误语义

03Execute & trace

权限复核 · 结果句柄 · 审计 · 产物

披露与调用的三层边界

可见性边界

普通 Chat 不注册函数;Agent 模式也只看到当前任务可能需要且已经启用的工具族。MCP 与 Skill 的旧描述不能替代当前 Capability Snapshot。

契约边界

工具名称、JSON Schema、参数上限、错误语义和结果句柄按需展开;大结果优先返回摘要、文件或 Artifact 引用,不把完整 payload 塞回上下文。

授权边界

工具对模型可见不代表一定可执行。每次调用仍由服务端重新验证 Membership、workspace permission、资源作用域、Grant 和 Provider readiness。

审计边界

调用参数使用脱敏摘要持久化;成功、拒绝、超时、截断和 unavailable 都是规范结果,并与 MessagePart、Invocation 或 Sandbox Command 关联。

i
原子工具的边界

一个工具只负责一个可验证动作,例如读文件、写文件、列举文件、执行命令、转录或发布产物。组合逻辑留给 Agent;授权、幂等、审计和结果裁剪留在服务端。

来源:backend/app/services/agent_runtime.py · backend/app/services/mcp_skills.py · backend/app/services/sandbox.py
15A

Atomic capabilities

工具是权限边界,不是模型的万能遥控器

READ观察

read_file、list_files、subapp_observe 等只读能力返回有限摘要,不直接暴露不可信 payload。

WRITE修改

write_file、publish_file 等改变状态的能力需要明确作用域、大小限制和可追踪结果。

RUN执行

exec、transcribe_audio 运行在受控 Provider/沙箱中,保留超时、退出码、截断和审计。

APPROVE破坏性动作

删除、外发、网络访问等操作与 Grant/egress approval 绑定,拒绝也是完整结果。

capability map→discover contract→enabled + grant→schema validation→execution→normalized result→audit / MessagePart
原子工具属性服务端要求失败时行为
单一动作读、写、列举、执行、转录、发布分别建模,不提供绕过策略的万能 shell 工具返回稳定错误码,不猜测替代动作
最小输入路径、ID、argv 和选项均有类型、长度、数量与作用域限制参数校验失败时不进入执行器
最小输出输出裁剪并优先产生结构化摘要、句柄或 Artifact;敏感字段先脱敏截断和脱敏状态随结果返回
可重试语义改变状态的动作使用幂等键或 revision/CAS,避免模型重试造成重复副作用冲突、重复和授权挑战保持可区分
来源:backend/app/services/agent_runtime.py · backend/app/services/sandbox_authz.py · backend/app/services/egress_approvals.py

Prompt compiler

系统提示词策略

提示词由多个 system message 分层编译,而不是把所有内容拼成无法审计的单块文本。 核心规则稳定,工作区风格和本次授权上下文动态注入。

优先级 1
Core Instructions

身份、真实性、安全、隐私、指令优先级与通用回答行为。

优先级 2
Response Style

基础风格 + 温和、热情、标题、Emoji、详细度五个离散特征。

优先级 3
Mode & Tool Policy

普通 Chat 禁止工具;Agent 模式允许使用本次注册的函数。

优先级 4
Agent Stream Guidance

工具前给简短可见说明,工具后可更新进展,结束时给完整答案。

优先级 5
Authorized Context

选中节点、附件、文档选区与最多 8 个已授权 Skill 包。

优先级 6
Durable History + User Turn

协议有效的历史消息、必要的上下文压缩,以及当前用户输入。

风格策略

工作区可选择 default、professional、friendly、candid、 efficient、exploratory、quirky 或 cynical。 五个附加特征均使用 -2…2。风格只控制表达方式,不得改变事实、工具权限或用户当前明确格式。

提示词注入防线

参考数据降权

文档与网页摘录明确视为不可信参考,不作为系统指令。

授权上下文

只注入当前用户和工作区有权读取、且与本轮相关的内容。

Skill 限额

单包最多 8,000 字符、每轮最多 8 个;脚本不会因提示词自动执行。

上下文压缩

接近窗口阈值时保留协议有效的最近后缀,旧事务用持久摘要表达。

来源:backend/app/prompts/fragments.py · backend/app/prompts/compiler.py · backend/app/services/chat.py
09

Tool registry

Tool 注册与调用

Tool 是给模型看的 JSON Schema 函数定义与服务端执行器的配对。注册本身无副作用; 只有模型输出 tool call 后才进入参数校验、权限检查和执行。

工具族代表能力写入边界
会话与时间检索 Session 片段、读取历史区段、读取当前时区时间只读
图谱读取目标/能力图谱、候选节点更新、提出图谱变更正式图谱须用户审核
学习闭环路线读取/重规划、行动计划、Mastery 读取、Evidence 草稿Evidence 只写 pending
Provider列 Provider/模型、能力查询、配置与 Secret 轮换管理写入要求 workspace.manage
Websearch_web、parallel_web_research仅在 SearchProvider 可用时注册
内容产物图片生成、可信组件、Magic Card、沙箱文件 Artifact不允许任意代码进入宿主 DOM
扩展声明式 Skill、已授权 MCP 工具沿用各自 Grant 与 Invocation 边界

调用结果

执行器返回规范化的成功或失败对象,并可附带来源、Artifact、Extension Invocation ID、Sandbox Session ID 等安全元数据。意外异常不会把 SQL、HTTP、Provider 或沙箱实现细节泄露给模型;权威堆栈保留在服务端日志。

Normalized tool result
{
  "status": "completed",
  "data": { "…": "domain result" },
  "meta": {
    "extension_invocation_id": "…",
    "source_count": 3
  }
}
10

Instruction & workflow extensions

Skills

LearnGraph 区分两类 Skill。它们的安装、授权和运行语义不同,不能把文件包中的脚本当作自动 Tool。

Agent Skill Package

指令型文件包

  • 至少包含 SKILL.md
  • 启用且具有 always Grant 后按需注入 system context
  • scripts/* 不注册为函数工具
  • 脚本仅可通过 Docker 无网 sandbox-run 显式试运行
  • 文件按 ContentBlob 内容寻址保存,禁止绝对路径与 ..
Declarative Skill

声明式工作流

  • 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.py
11

Model Context Protocol

MCP

MCP Server 是工作区级外部工具目录。运行时不会信任注册表中的旧描述: 每次调用前重新发现能力并保存不可变 CapabilitySnapshot。

01Register端点、来源、认证引用
→
02Discoverinitialize + tools/list
→
03Authorize身份 + 快照 + 权限指纹
→
04Invoketools/call + audit

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 在调用前消费,即使远程执行失败也不会恢复。

i
Agent 可见性比“注册过”更严格

只有当前快照 enabled 且具有明确持久 always Grant 的 MCP 工具才进入 Agent Tool Registry。

12

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 严格互斥。

LearnGraph host
SandboxAgentWorkspaceServiceargv 策略 · 授权 · 配额 · 审计 · 计费桥(ASR)· Artifact
Sandboxd API(版本化 · bearer 认证 · ownership scope)→ Docker boundary · network=none · read-only rootfs · uid 65532
Unified Runner 镜像 Python 3.12 + Node + Chromium + ffmpeg · digest pin(sandboxd Bootstrap 拉取+冒烟)
/workspace named volume inputs / work / outputs · 每沙箱独立 volume · 用户×会话隔离

容器硬化基线

网络与文件系统

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

镜像 digest pin

镜像必须以 @sha256: 固定;未 pin 时 probe 直接不可用。基础镜像同样按 digest 固定,Bootstrap 冒烟通过后才原子落盘。

执行环境净化

Python 强制 -I -B 隔离模式;PIP_NO_INDEX=1、代理变量置空;前端工具链预装在镜像 /node_modules,零下载、不占工作区配额。

生命周期与双 TTL

状态机 · services/sandbox.py + core/scheduler.py
CREATED → COLD → STARTING → RUNNING → WARM_IDLE → (COLD | EXPIRED)
# STARTING 先落库占位再调 Docker,容量统计含 STARTING/RUNNING/WARM_IDLE
# 超时 kill:容器被丢弃回 COLD;工作区数据保留
# 清理调度器对 STARTING/RUNNING 会话不动 bind mount(看门狗保护)
维度闲置 TTL绝对 TTL到期动作
容器(runtime)180s1800s删除容器,会话降级 COLD(cool-down,工作区保留)
工作区(workspace)1800s(活动可外推)86400s(创建即定死,不可外推)删容器 + 二次逃逸校验后 rmtree,会话 EXPIRED

资源护栏

180sWall time
2 CPUCPU 配额
2 GiBMemory(禁 swap)
512PIDs(含线程)
256 MiBWorkspace disk
5 MiBOutput 截断

磁盘配额是双机制:内核级 fsize ulimit 限单文件 + 宿主侧聚合统计在写入前后与执行轮询每一拍检查。 宿主层另有保护:单用户 2 个活跃容器、部署级 20 个、宿主内存分配 ≤70%、CPU ≤80%、保留磁盘 ≥20 GiB、 单用户留存工作区 ≤10 个。默认值见 backend/app/core/config.py(LEARNGRAPH_SANDBOX_*)。

一次 Agent 命令的执行管线

  1. 01
    argv 策略

    解释器白名单(python/node),禁止 -c/-m/-e/--eval/-p/-i 内联代码;入口必须是工作区内相对 .py/.js 文件;cwd 只能是 .;参数数与单参字节受限。

  2. 02
    破坏性删除(默认免审批)

    rm / dd / mkfs / Remove-Item 等命令仅允许 work/ 子树(绝对 / .. / 盘符路径直接硬拦);默认 LEARNGRAPH_SANDBOX_DELETE_APPROVAL_MODE=off 直接放行并保留审计(沙箱隔离,不影响宿主机文件),设为 on 时恢复会话级单次 Grant(TTL 60s–24h),缺失则 403 授权挑战经 SSE 弹窗回传。

  3. 03
    幂等与审计

    Idempotency-Key 去重;持久化 SandboxAgentCommand 只存 argv 摘要与脱敏副本,stdout/stderr 落库前同样脱敏。

  4. 04
    容量与容器

    进程锁 + 跨进程文件锁下校验用户/宿主容量,先提交 STARTING 占位再 docker create;失败回滚 COLD。

  5. 05
    快照 → 执行 → 恢复

    执行前对工作区做 inode 级快照;100ms 粒度轮询超时与聚合配额;执行后凡未授权的删除一律从快照恢复并抛授权挑战——argv 拦不住的 shutil.rmtree 类软删除由此兜底。

  6. 06
    结果与 SSE

    exit code / 超时 / 截断 / 延迟落库;结果转成 sandbox_status / sandbox_artifact MessagePart 流式呈现。

双层存储:物理工作区 + 逻辑工作区

物理层(bind mount)

宿主 {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.manageAgent 会话、命令执行(幂等)、文件读写列举
/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 端到端脚本。

i
三个不同的 “sandbox”

本节指 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/
13A

Egress approval chain

沙箱联网与审批:默认断网,出网必须过审

沙箱没有直接出网。需要联网的能力(网页抓取、依赖下载)必须经过工作区级审批链; 没有合法策略时容器保持 network_mode="none"(fail-closed,默认拒绝网络是安全基线)。

EgressPolicy 白名单

按工作区评审的出网策略:host 域名 + HTTPS 端口 + 有效期 + 审批记录 + 不可变 digest;拒绝通配符、拒绝 IP 字面量。抓取白名单(web_fetch.policy)与通用 Agent egress 策略各自独立,抓取授权不会扩大通用出网面。

CONNECT 代理唯一出口

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;跨主机重定向必须再次审批。

图片 URL

限制 wire bytes、像素与帧数,只接受 PNG/JPEG/WebP;完整解码后重新编码,去除 EXIF/ICC 等非必要元数据,再写入内容寻址存储与会话 inputs/。`search_images` 之后可传 `urls` 数组(2–8 张)并行下载,单张失败不中断整批。

GitHub 源码

通过 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 日志清洗为可读行, 进度持久化、只前进不倒退、刷新不回跳;前端只渲染、不模拟。

i
联网能力与沙箱能力分开授权

网页抓取的白名单、审批记录与沙箱执行权限是独立维度;抓取授权只影响抓取路径,不会放大 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.py
13B

Host 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。

Host Service Bridge(默认)

真机常驻授权反向代理(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 文件,容器侧零配置。

Trusted Desktop Direct(opt-in)

LEARNGRAPH_HOST_ACCESS_MODE=direct:容器内 loopback 的 Provider/MCP 地址改写为 host.docker.internal:同端口(端口/路径/query 原样保留),Docker Desktop 把该别名转发到真机同端口。无注册、无 token、无审计——主应用可触达宿主全部本地 TCP 端口,仅建议 Windows/macOS Docker Desktop 个人可信环境;非容器(源码安装)自动回退为不改写。

off / 源码模式

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 原样返回(源码直连)
direct 模式启用(仓库根目录 .env)
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.tsx
13C

Atomic memory

记忆系统:原子记忆

记忆不是"把完整历史塞 prompt"。LearnGraph 的记忆系统以原子记忆为单位: 自动沉淀对话与任务中值得长期记住的事实,并在后续会话中按相关性回填给 Agent。 本节只描述当前提供的能力;底层架构仍在演进中。

自动提取

对话与任务结束后自动识别值得长期记住的事实(用户偏好、项目决策、纠错反馈等),无需手工录入;由工作区"记忆"总开关控制。

原子化存储

每条记忆独立成条,记录来源与置信度;同一事实的多次修正更新原条目,而不是堆积重复内容。

按需回填

后续会话只装配与当前任务相关的记忆,不注入完整历史;受预算约束,避免上下文失控。

用户纠错优先

用户可查看、修正或删除记忆;用户明确给出的纠正优先于自动提取的结果。

作用域隔离

记忆按任务 / 工作区 / 用户分级,跨工作区访问受授权约束。

时效管理

记忆带时效信息,过期或失去依据的内容自动降级或失效,避免误导后续会话。

13D

Artifact gateway

产物与分享:不可变版本、分享令牌与卡片索引

沙箱、Agent 和可信组件产生的产物(sandbox_artifact、组件 Artifact、子应用 bundle)统一经过 Artifact Gateway 管理,遵循"发布即冻结"的版本模型,而不是工作区临时文件。

不可变版本快照

发布时生成不可变版本(版本 + 内容 hash 绑定),同一版本不可替换;预览与分享都引用版本而非工作区路径,工作区过期不影响已发布产物。

只读分享令牌

分享生成令牌(token),只读公开访问;令牌可撤销,撤销后原链接立即失效。

软删除 + 级联撤销

删除为软删除(保留审计痕迹);删除产物时级联撤销其分享令牌,避免"删了还公开着"。

会话卡片索引

产物自动聚合为会话卡片(卡片网格),按会话/会话类型索引,可在产物页统一管理(编辑、分享、删除)。

渲染降级

组件 Artifact 由服务器持有模板渲染(CSP 锁死,见可信组件节);离线沙箱渲染可用时附加有界截图,不可用时 runtime_status=unavailable 安全降级。

bundle 预览

交互式子应用支持 bundle 预览地址(独立 preview 源),发布/预览到期校验做时区归一化。

来源:backend/app/services/artifact_gateway.py · artifact_cards.py · canvas_cards.py · component_renderer.py · backend/app/api/routers/artifact_gateway.py
14

Streaming contract

消息与 SSE

SSE 是传输层,不是事实源。Session、Message、MessageVersion、MessagePart 与流事件均持久化, 从而支持刷新读取、断点续传、分支、版本比较和 Provider 协议续接。

user_message持久化输入
reasoning_delta仅 Provider 暴露的 summary
tool_call参数与调用状态
tool_result规范化结果 / Artifact
text_delta可见回答
completed最终版本与用量

前端以 Message Part 渲染文本、来源、推理摘要、工具状态、可信组件和沙箱 Artifact。 未知组件安全降级为数据预览,不注入任意 HTML、脚本或 React 代码。

单写者下的落库工程

SQLite 只有一个写者,如果每个流 delta 都 commit,token 投递会被串行化在 fsync/锁获取后面。策略:

分级 flush

started / completed / failed / checkpoint 等锚点事件立即落库;普通 delta 按条数或时间节奏批量落库——可靠性与吞吐兼得。

per-Session 串行化付费调用

自动标题、建议、活动摘要等每次调用都花钱的 LLM 调用按 Session 加锁串行,并发请求先释放快照再 CAS 读,防重复付费。

重试分类与退避

502/503/504 为中继失败、500 flaky、408/429 明确可重试、529 overloaded;退避用 uniform ±ratio 抖动 + 上限,避免惊群。

共享 executor 工具超时

Agent 工具调用用模块级共享单 worker executor + 宿主超时;超时后链路继续,挂起线程不阻塞回合。

心跳、断线与恢复

SSE 心跳

工具执行 / 退避 / 搜索期间定期发心跳,防止中间代理断流,也防止用户误以为卡死。

状态机终端化

后端重启把未完成流标为 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.ts
15

Trusted components

可信组件

可信组件是助手消息流里的声明式 UI 卡片(选择题、填空、天气卡、指标卡、图片框等)。它用 JSON Schema 声明数据面,而不是把任意 HTML/React 注入宿主 DOM。Agent 可通过 canvas_emit_trusted_component 发布内置组件,也可注册并授权第三方组件 Manifest 再发布。所有写操作经后端 Schema 守卫、哈希与审计, 前端对未注册或未通过 Schema 的组件安全降级为 JSON 预览,绝不执行脚本。

Channel A · 声明式

内置目录 · 宿主 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 默认的表单 / 展示通道
  • 权限由系统白名单固定,工作区不可改写
Channel B · 沙箱

第三方 Manifest · 隔离浏览器

  • 任意 component_id,经工作区导入 Manifest 注册
  • 渲染器固定为 sandbox,结果以 sandbox_artifact 交付
  • 条件可用:依赖未配置的隔离浏览器渲染器,缺失时 runtime_status=unavailable 安全降级
  • 需工作区显式授权后才可发布或生成 Artifact

协议生命周期

register→health_check→authorize(+enable)→artifact / event_validate 版本、Schema、权限或包哈希变化会废止旧授权,需重新授权;同一 version 的 Manifest 不可替换。
  1. 01
    register

    提交完整 Manifest,服务端做静态 Schema 守卫、签名记录与 example_data 校验,落 Plugin 与 ManifestVersion。

  2. 02
    health_check

    对当前 Manifest 跑 JSON Schema 校验(render 检查在隔离浏览器未配置时标记 unavailable,不阻断)。

  3. 03
    authorize + enable

    工作区对当前版本 + 权限指纹授权并启用插件;内置组件由系统固定,工作区不可改。

  4. 04
    artifact / 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 保留,第三方不可占用。

version

SemVer,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,同样要求顶层闭合对象。

permissions

network_domains(精确 DNS 主机,禁通配/带 scheme)、file_read、clipboard_write、message_actions(小写标识)。

size_limits

min_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

可选字段省略即可,不要传 null;后端对内置 props 会做 null 剥离。

内置组件最小合法 props

option_group / single_choice / multiple_choice
{
  "title": "你更希望怎样验收这次学习?",
  "description": "单选一项后继续",
  "options": [
    { "id": "project", "label": "完成一个小项目" },
    { "id": "explain", "label": "能够清楚讲解" }
  ],
  "allow_custom": true,
  "allow_skip": true,
  "submit_label": "确认并继续"
}
fill_blank
{
  "title": "请补全 ACID 中的 A",
  "prompt": "ACID 中的 A 代表 ____",
  "placeholder": "Atomicity / 原子性",
  "multiline": false,
  "submit_label": "提交",
  "blank_ids": ["answer"]
}
short_answer_table
{
  "title": "简答题",
  "columns": ["问题", "你的回答"],
  "rows": [["为什么需要索引?", ""]]
}
weather_card
{
  "title": "杭州明日天气",
  "location": "杭州",
  "condition": "多云",
  "temperature_c": 27,
  "high_c": 29,
  "low_c": 21,
  "summary": "适合户外轻量复习",
  "unit": "C",
  "actions": [
    { "id": "create_plan", "label": "生成明日学习计划", "event": "create_plan" }
  ]
}
metric_card
{
  "title": "今日学习指标",
  "description": "来自当前目标进度",
  "metrics": [
    { "id": "mastery", "label": "掌握度", "value": "62%", "hint": "近 7 日" },
    { "id": "reviews", "label": "待复习", "value": 3 }
  ]
}
image_frame
{
  "title": "示意图",
  "alt": "B+ 树结构示意",
  "status": "placeholder"
}

完整第三方 Manifest 示例

以下是一个自定义 quiz_card 的完整 Manifest,可直接用于 POST /api/v1/plugins/components 或智能体的 component_register_manifest 工具。它满足全部 Schema 安全约束:顶层闭合对象、无禁用字段、 无 pattern / 外部 $ref、example_data 能通过自身 data_schema。

quiz_card Manifest · POST /plugins/components
{
  "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 权限时,可直接在对话里创建并应用可信组件,无需切到设置页:

  1. 01
    component_register_manifest

    提交第三方 Manifest,服务端做静态与健康检查并落审计;内置 ID 被拒绝。

  2. 02
    component_authorize

    对刚注册的 plugin_id + manifest_version_id 授权当前工作区并启用插件(enable=false 可仅授权不启用)。

  3. 03
    component_list

    查询已注册组件及其授权/启用状态,决定下一步是授权还是发布。

  4. 04
    canvas_emit_trusted_component

    对自定义 component_type 传其 component_id 发布;已授权则按 sandbox_artifact 交付。

!
第三方组件渲染是条件可用

第三方组件渲染器固定为 sandbox,依赖隔离浏览器渲染器;当前未配置,发布会以 sandbox_artifact 安全降级(runtime_status=unavailable)交付,不会把任意代码注入宿主 DOM。 内置 8 类组件不受此限制,端到端可用。

API 面

端点组权限职责
POST /plugins/componentsworkspace.write导入并校验第三方 Manifest(注册)
GET /{plugin_id}/manifests · /authorizations · /checksworkspace.write列出版本、授权与检查记录
POST /{plugin_id}/authorizations · /authorizations/revokeworkspace.write授权或撤销当前工作区
POST /{plugin_id}/checksworkspace.write触发 health / render 检查
POST /{plugin_id}/artifacts已授权按授权边界生成受控 Artifact
POST /{plugin_id}/events/validate已授权按 event_schema 校验用户事件
i
Agent 工具与 HTTP 复用同一服务

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.md
15B

Interactive sub-apps

交互式子应用:有状态的双向数据通道

交互式子应用(subapp)是可信组件的进阶形态:不只展示,还能收集用户交互数据并接收 AI 写回的状态, 形成"用户操作 → 事件 → Agent 处理 → 页面更新"的多轮对话式交互。UI 与数据契约分离—— interaction_contract 定义数据格式,"千万种交互形式,结果一致"。

协议与生命周期

  1. 01
    注册 bundle

    subapp_bundles 带 interaction_contract 与 component_manifest_id;契约校验顺序与组件 ID 匹配在服务端强制。

  2. 02
    发布 / 预览

    发布产生不可变版本与 bundle 预览地址(独立 preview 源);发布/预览到期校验做时区归一化。

  3. 03
    双向通道

    iframe → postMessage 数据出口(宿主桥)→ 事件入库(T1.4 只读观察)→ 事件驱动 Agent 处理 → patch_state 写回(T2.5)→ 页面更新。

  4. 04
    媒体注入

    视频 / 音频经 blob 注入(零联网);地图等外部资源走经审批的会话级媒体域名。

安全不变量

服务器持有模板

预览 HTML 是固定模板,组件数据是 schema 校验后的惰性 JSON,渲染进 HTML-escaped <pre>;组件提供的 HTML/JS/CSS/URL 永不嵌入("没有可执行面")。

CSP 锁死 + opaque iframe

default-src 'none'; script-src 'none'; connect-src 'none';iframe 读不到 cookie/localStorage,发不出 HTTP。

postMessage 唯一出口

CSP 拦不住 postMessage,因此数据出口只有宿主桥一条受控通道,按 interaction contract 校验、可审计。

渲染兜底

组件渲染可选在离线 Docker 沙箱做校验 + 有界截图;不可用则 runtime_status=unavailable 降级,不阻断主流程。

i
演进不破坏安全

路径是 静态 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.ts
16

Security model

安全与授权

安全边界贯穿路由、服务、扩展授权、Provider Secret、沙箱和输出渲染,而不是单独依赖前端隐藏按钮。

01Workspace scope

所有资源查询由后端重新校验 Membership、Permission 与 ACL;跨工作区 ID 返回不可枚举错误。

02Secret isolation

Provider Key 由后端加密保存,不进入浏览器、日志、SSE、审计或导出。

03Exact grants

MCP/Skill 的权限集合必须与服务端推导的最小集合完全一致,少一项或多一项都拒绝。

04Human review

正式目标图谱的重要变更、删除影响与高风险执行保留用户确认。

05No silent fallback

远程 Provider、Docker 或 Transport 失败时保持失败事实,不落回 mock、宿主执行或硬编码结果。

06Audit trail

授权、拒绝、撤销、失败、超时、大小阻断和成功调用均记录工作区审计。

认证、供应链与发布治理

认证加固

首次登录强制改密(独立页面 + 刷新恢复)、认证限流防灌水;按端点明确权限边界、避免尾斜杠绕过。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—— 任何"静默降级"都被视为缺陷。

17

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 与 Invocation
backend/app/services/memory_*.py原子记忆:提取 / 存储 / 检索 / 回填 / 纠错 / 时效管理
backend/app/services/sandbox.py沙箱任务与 Agent Workspace
backend/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 输入输出 Schema
mobile/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 后台生成端点
18

Extension guide

扩展指南

新增 Provider

  1. 01
    定义 Port

    优先复用现有 Protocol;不要让业务 Service 依赖厂商 SDK。

  2. 02
    实现 Adapter

    显式建模可用性、超时、响应校验、远程能力和安全诊断。

  3. 03
    接入 Catalog / Probe

    配置保存后执行真实探测,失败保持 disabled。

  4. 04
    记录 Trace / Usage

    保存 Provider ID、请求标识、用量与安全错误分类。

  5. 05
    真实验收

    用部署方明确配置的远程服务验证,缺凭据时标记未完成。

新增 Agent Tool

  1. 01
    从领域用例开始

    Tool 只调用现有 Service 或受控 Port,不直接访问数据库句柄。

  2. 02
    声明严格 Schema

    限制字段、长度、枚举与 additionalProperties。

  3. 03
    确定授权与副作用

    按用户角色、工作区、资源范围与是否需二次确认注册。

  4. 04
    规范化结果

    裁剪大小、脱敏错误、记录 Invocation / Audit,并返回可追踪来源。

  5. 05
    验证完整循环

    真实模型发起调用、真实服务执行、刷新后仍可读取结果。

19

Verification

验证与完成定义

“现有测试通过”不等于业务闭环已完成。重要变更需要同时验证可见结果、后端事实和一个高风险失败/恢复边界。

Verification commands
# 常规检查(不自动包含 E2E 与真实远程 Provider)
npm.cmd run check

# 真实浏览器 E2E
npm.cmd --prefix frontend run test:e2e

# 已配置真实远程模型后
.\scripts\verify-real-provider.ps1
从真实目标到可追溯成长

保持来源、权限、事务、审计和用户审核边界一致。

已复制到剪贴板