DSHarness 系统拆解 固定基线 47f943859b · 36 已复核 / 0 撰写中 / 36 章
English
可靠性与产品面·第 31 章

安全模型与信任边界

本地 Agent 产品怎样将自由执行收敛为显式策略

已复核上游 47f943859b范围: 建立 threat model,覆盖配置信任、模型输入、工具 JSON、审批、沙箱、路径、凭据、远端与自修改。

结论:这是 capability policy,不是敌对租户沙箱

DeepSeek Harness 先把普通模型输出当作不可信数据,再通过本地 operator 选择的 capability graph,把其中一部分请求转换成真实权限。它最强的控制包括显式组合、运行期 schema 校验、按 Session 持久化的 permission facts、单次升权、文件副作用约束、路径 containment,以及各 carrier 自己的请求围栏。它并不试图隔离恶意本地插件、同一 OS 用户下的敌对进程、已受信任的远程客户端,也不试图在 operator 主动开放 bash-equivalent execution 后继续隔离模型编写的代码。

因此决定安全性的并不是“Agent 有没有 sandbox”,而是“哪个 principal 选择了这项能力、哪个边界校验其输入、哪个机制真正实施约束,以及约束之后仍依赖什么信任假设”。实现对这些问题的回答并不均匀:一些边界会 fail closed,另一些则有意依赖本地 ownership、进程隔离、第三方基础设施或 package supply chain。

1. 威胁模型:五类 authority,以及“trusted”的三种不同含义

Principal 或输入默认立场适用控制仍被信任的部分
模型文本与 tool-call JSON不可信数据解析、schema 校验、可见性限制、policy hooks、guards、结果规范化被选中的 tool body 及其可达的每项 capability
本地 operator、继承环境、profile 与 patch 文件管理权限分层优先级、发现到的 .env 中 bootstrap 变量拒绝、加载期校验、事务化启动被有意插入进程内 plugin tree 的全部内容
workspace 文件、skills、MCP metadata 与 provider 响应信任程度取决于 provenance 的内容格式校验、invocation flags、JSON 投影、timeouts指令语义、外部 server 行为以及任何显式授予的 credential
Browser、Remote、SDK 与 ACP 客户端由 carrier 决定的 controllerHost/Origin 围栏或私有 stdio、typed methods、ownership checks可达性或进程 ownership;不存在共同的 user-authentication 层
OS sandbox runner、filesystem 与 E2B service实施约束的底座kernel/ACL profiles、canonical paths、atomic operations、remote lifecycleOS、配置的 runner、远端服务与部署组合
安全属性

目标属性是 capability confinement:不可信模型调用不应超出 operator 选择的 capability 与 policy。它不是针对已经控制配置、插件、进程环境或受信 transport endpoint 的 principal 所做的 multi-tenant noninterference 承诺。

2. 启动配置是管理边界,不是被动数据

CLI 按固定顺序快照三层环境:继承的 process environment、调用目录的 .env、Harness home 的 .env。继承值优先。两个发现到的文件会先全部解析,再物化任意一层;denylist 会阻止能重定向进程启动、module loading、VCS 行为、proxy、certificate、provider endpoint 的变量,以及所有 DSH_/XDG_/DYLD_ 前缀。其余接受的值随后写入 process.env,但不覆盖继承值。SEC-ENV-LAYERS

User patch 与 overlay 文件比 .env 权限更强:它们可以替换配置、插入 Loader rows,并包含 !!js expression。存在但不可读、不可解析或形状错误的 layer 会使启动失败,而不是被跳过。SEC-PATCH-LAYERS Root include 在配置旁解析相对插件,并通过配置的 module base 解析 bare plugin,随后要求 Loader 创建得到的 tree。SEC-LOADER-IMPORT

启动在 lifecycle 意义上是事务化的:prepare、mount、settle 与 activation audit 依次执行;任何失败都会 dispose 部分 root,并保留最深层 error stack。SEC-BOOT-TRANSACTION 这保护 availability 与 cleanup,却不会让配置的插件变安全:成功 import 的插件在 Host process 内运行,并获得其组合暴露的 Services。

3. Profile 与插件安装把本地 package chain 变成可执行 policy

有效 profile 依次由 bundle patches、profile patches、home patches、command-line overlays 与 launcher-owned overlays 组成。因此,后置管理层可以修改或插入定义 runtime capability graph 的 rows。SEC-PROFILE-ORDER

dsh plugin 有意保持为薄的 pnpm forwarder。它在 profile directory 中运行 operator 请求的 package-manager 命令;成功后,任何声明 bundle patch 的已安装 dependency 都会自动加入 profile 的 active layer list。相对 package spec 锚定到 operator 发起命令的目录;git package 的 build policy 交给 pnpm。SEC-PLUGIN-PNPM

控制

加载失败会明确报错,profile reconciliation 以已安装状态为准,失败的 package-manager 命令不会激活新 bundle。

假设

Package provenance、install scripts、transitive code 与 bundle patch 作者都被当作本地 administrator code 信任。成功安装与进程内激活之间不存在额外的产品 sandbox。

4. 模型 tool JSON 穿过多层 validator,但 validation ownership 并不统一

Agent loop 把每个模型 tool block 转成 execution request。合法 JSON 被解析,空 argument string 映射为 {};非法 JSON 则有意保留为 raw string,让最终工具能够给出有用的 validation error,而不是丢失模型写出的输入。SEC-MODEL-PARSE

defineTool() 构建的一方工具会编译 parameter 与 output schema。进入用户 body 前,wrapper 立即校验 arguments 并抛出 ToolArgsError;replay presenter 只做 soft validation,因为历史 arguments 可能遵循旧 schema。SEC-TOOL-DEFINE 更底层的 registry contract 不同:register() 校验 output contract、timeout 与 reserved name,但不会给任意 ToolDefinition.execute 自动包一层通用输入校验。SEC-RAW-TOOL-REGISTER

Policy 运行前,registry 会把 arguments 快照为 lossless JSON,并 deep-freeze 分离后的值。非法或 getter-hostile 的值会物化为 error result;调用方无法在 policy 检查后再修改该 object。SEC-TOOL-ARG-SNAPSHOT

5. Authorization 是有序 pipeline,不是统一确认弹窗

对一个 ready call,ToolRuntime 先运行 scoped tools/pre-execute waterfall,其默认结果是 allow。只有 ask decision 才会调用 approval service。得到 allow 后仍要经过 monotonic guards;任何 denial 都会在 dispatch 前物化,body 开始前还会再次检查 cancellation。SEC-TOOL-GATE

Approval service 要求 turn 仍处于 open 状态,先追加 approval/asked event,解析 policy 与 answerer,再追加 approval/decidednever 会在 answerer dispatch 前拒绝;缺失、抛错或返回词汇表外结果的 answerer 都变成 unavailable;取消会丢弃迟到答案。allowed-once 是唯一 grant。SEC-APPROVAL-CLOSED

控制

Approval question 会 fail closed 且可审计;guard 也不能把其他位置已经拒绝的调用强制改成 allow。

边界

Approval 是由 consumer 触发的 policy,不是包围每个工具的强制 gate。已被 standing sandbox 与 tool policy 允许的普通动作会直接执行而不提示。增加 answerer 不会把 runtime 变成默认拒绝授权。

6. Permission preset 固定 standing authority;升权是另一项单次 grant

Preset service 要求 shell 公布 sandboxMode policy capability,并要求 approval service。内置表把 workspace-write 映射到 ask,把 danger-full-access 映射到 never;发布 settings 前会校验配置的 default。SEC-PRESETS-VALIDATE 新 Session 会获得 durable preset、sandbox-mode 与 approval-policy facts;seeded 或部分初始化的 Session 保留已有 facts,只补齐缺失项。SEC-PRESETS-PIN

发行的 base composition 还加入第三个 read-only + ask 选项;除非继承的 DSH_PERMISSION_MODE 指定别的 mode,fresh Session 从 workspace-write + ask 开始。danger-full-accessnever 配对,是因为前者已经是一项 standing file-access grant,而不是因为 never 会批准任何事情。SEC-SHIPPED-PERMISSION

Shell 与 filesystem escalation 共用一套更严格的 choreography:请求 mode 必须比本调用当前 mode 更宽;approval service 与 Agent 必须存在;reason 与精确 call identity 被审计;只有 allowed-once 会把更宽 mode 印到这一次调用上。其他所有结果都不会执行。SEC-ESCALATION

7. “Sandbox”只表示 process file-effect policy,不表示更宽的隔离

Sandbox seam 只定义三种 file-effect mode。Provider 必须返回 wrapped argv、enforcement level 与 diagnostic dialect;没有可用 backend 时必须 fail closed。Contract 明确把 network access 与 process visibility 留在词汇表之外。SEC-SANDBOX-SEAM

官方限制还更广:没有 syscall、device 或 credential restriction;confinement 只在 same world 中成立;denial classification 依赖 stderr;runner diagnostic 是 in-band,因此 child 可以模仿 failure signature、混淆诊断,但不能借此逃逸 confinement。SEC-SANDBOX-SCOPE

Local provider 在 Linux 选择 bubblewrap 后备 Landlock,在 macOS 选择 Seatbelt,在 Windows 选择 ACL runner。Bubblewrap 与 Seatbelt 声称 full file-effect enforcement;经过 probe 的 Landlock launcher 会按 ABI support 报告 full 或 partial,Windows 则因 ACL 与 hard-link semantics 无法满足绝对承诺而标为 partial。SEC-LOCAL-RUNNERSSEC-LANDLOCK-PROBE 配置 custom runner 会跳过平台选择;在仅校验 command 与 failure signature 形状后,其 full enforcement 被当成 operator 的声明。SEC-CUSTOM-RUNNER

已知缺口

被约束为 workspace write 的 command 仍可读取 host 文件、使用网络、枚举可见 process、访问保留下来的 ambient variable,并调用可达服务。“Workspace-write”绝不能被描述成 general-purpose isolation。

8. Filesystem policy containment mutation;read 与若干 race 仍在边界外

Sandboxed filesystem 原样继承 local read,只围住 writeTexteditTextread-only 拒绝 mutation;workspace-write 在 operation 前立即重新解析 target,并把 fresh canonical path 与 workspace/temp roots 比较;danger-full-access 不加围栏直接委托。源码明确接受最终检查与 syscall 之间剩余的 ancestor-symlink race。SEC-FS-FENCE

Local filesystem 的 cwd 只是 resolution default:absolute path 与 .. 都能逃出。Mutation lock 只在进程内生效;guarded replacement 可与其他进程竞态;version token 依赖 filesystem metadata;commit 后 cleanup 失败可能留下私有 staging residue。SEC-FS-LOCAL-LIMITS

Observation policy 增加了有价值的 stale/unseen-write gate,但它验证的是 freshness,不是 authorization。其状态不会跨 resume 保留;直接 ctx.fs read 不形成 observation;任意 windowed read 在版本未变时都能授权整文件 overwrite。SEC-OBSERVATION-LIMITS

控制

Canonicalization、separator-aware containment、atomic publication、version checks 与 observation policy 显著降低意外 path escape 与 lost update。

缺口

它们不保护文件 confidentiality,不抵御同一用户下的敌对进程改变 path topology,也不消除所有 cross-process replacement race。更强的 adversarial containment 需要 descriptor-based traversal 或 OS boundary,而不是再加一层 lexical check。

9. 配置面使用 credential reference,但 secret 仍属于 process authority

Credential seam 让配置携带 environment-variable-shaped reference,而不是 value。Consumer 每次 operation 都重新解析;describe 只暴露 configured/source/writable facts,setunset 则拥有 durable change。SEC-CREDENTIAL-SEAM

Local provider 定义明确优先级:继承 environment、受管理的 $DSH_HOME/.credentials.yaml、project .env、home .env。受管理 document 不会物化进 process.env;尽管如此,project 仍被有意信任为 fallback credential 来源。SEC-CREDENTIAL-LAYERS 在 POSIX 上,existing credentials file 只要有任意 group/other permission bit,就会在读取前被拒绝;parse error 也避免引用包含 secret 的源码行。Windows 因这里无法表达 ACL 而跳过 POSIX mode 检查。SEC-CREDENTIAL-FILE

解析时,继承 environment 是 read-only 且优先,随后才查 managed snapshot 与 dotenv fallback。UI 不能制造“已覆盖继承 secret”的假象:describe 会报告不可写,set/unset 会拒绝 shadowed write。SEC-CREDENTIAL-RESOLVE

10. Child environment 减少 ambient leakage;provider egress 仍是显式信任决定

共享 subprocess baseline 会以大小写不敏感方式移除名字匹配 KEY|PASSWORD|SECRET|TOKEN 的全部环境变量,以及所有 DSH_*。它保留 PATHHOME、locale 与 proxy 等值,之后的 explicit environment overlay 可以恢复任何已移除值。SEC-CHILD-ENV

DeepSeek adapter 校验有边界的 request/model settings,快照 configured endpoint,并把该 endpoint generation 与同一次 request 解析出的 credential 配对;遇到非法 live settings update 时保留 last good configuration。Endpoint 字段本身只是 configured string,不提供 HTTPS、host allowlist 或 SSRF policy。SEC-PROVIDER-CONFIG 随后它把 model request 序列化,并连同 bearer key、attribution、anonymous user identity、可选 Session identity 与 compaction marker 发送到配置的 /chat/completions endpoint。SEC-PROVIDER-TRANSPORT

控制

Endpoint 与 key 不会意外来自不同 settings generation;credential 也不会被整批复制进普通 child environment。

已知缺口

按名称 scrub 是 heuristic,不是 data-flow tracking:命名为 COOKIEAUTH 或其他未匹配标签的 secret 会保留;explicit overlay 则有意绕过 scrub。配置的 model endpoint 被信任接收 credential 与全部序列化 prompt content。

11. MCP 是外部 capability provider,不是 local shell sandbox 的延伸

MCP stdio transport 通过上游 SDK 启动配置的 command,环境由 scrubbed parent environment 加 explicit override 构成。HTTP transport 连接到配置 URL,并发送配置 headers。两条路径都不经过面向模型的 shell executor,也不经过其 sandbox escalation flow。SEC-MCP-TRANSPORT

Discovery 会排空外部 server 的 tool list,并在触碰 registry 前构造完整的 server-qualified candidate map。随后它先 dispose 旧 generation,再顺序注册新 generation:该 server list 中重复的 tool name 会在 swap 前失败;registry conflict 则 dispose 部分注册的新 generation,并让该 server 暂时暴露零个 tool,而不是恢复旧集合。SEC-MCP-SYNC 执行时不会在本地根据 advertised input schema 校验 argument:null 与 primitive 被收敛为 {},array 与 object 则传给 server。Executor 校验或归一化 top-level result shape;Native rendering 防御性提取 text 与非文本摘要,但 canonical JSON content 会为 Code Mode 原样保留。MCP isError 被转换成 tool failure。SEC-MCP-EXEC

12. Browser reachability 有围栏,但 reachability 不是 user authentication

每个 Browser API request 都必须携带 loopback 或已配置的 Host authority。显式 cross-site Fetch Metadata 会被拒绝;如果携带 Origin,它必须匹配 Host authority。代码直接说明 binding 与 network reachability 是另一层问题,而且此围栏不是 authentication layer。SEC-BROWSER-FENCE

即使 trustedHosts 接纳 LAN authority,configuration、credentials、native Host actions、preset document/copy/remove actions 与 model discovery 仍固定为 loopback。同一源码也解释了为什么普通 Agent method 不被视作更低权限:能够 create Session 的客户端已经可以选择 preset,并在发行 capability graph 下以 Host process 身份运行 command。因此,该围栏防御 DNS rebinding 与 cross-site browser abuse,但不识别人类用户。SEC-BROWSER-PRIVILEGED

已知缺口

声明 non-loopback authority 等于向其授予非 pinned API surface 的管理可达性。在真正的 authentication 与 authorization layer 出现前,不应把它暴露给不可信网络。

13. Generated Remote 收窄 capability 与 shape,随后继承 Browser carrier 的信任

Client assembly 挂载固定的 generated Remote contributions;activation 失败或 assembly dispose 时按逆序撤销。Capability availability 因而在 build/composition 时显式选择,而不是从所有 live Host Service 中自动发现。SEC-REMOTE-MOUNT Client gateway 要求每个 parameter、result 与 contextual identity 都有 strict codec;boundary parser 会拒绝缺失 strict codec 或 schema parse failure。SEC-REMOTE-CODECS 实际 invocation 会取得 active Connection,再通过它的 /api RPC path 发送 generated endpoint,因此 Remote 没有引入独立 carrier identity。SEC-REMOTE-CARRIER

转发的 Host event 由一份显式 allowlist 控制。原始 event name 与 JSON argument list 原样跨越边界,不做 projection、redaction 或 renaming;增加一项 list entry 就扩大 observable surface。SEC-REMOTE-EVENTS

控制

Generated codec、strict descriptor、explicit mount 与 closed event list 降低意外 API 暴露与 malformed payload。

假设

Remote 不提供独立 identity boundary。Call 与 event 复用 Browser API 的 Connection authority,因此 type safety 证明的是 payload shape,而不是谁有权调用 capability 或查看 allowlist 中原样转发的 event。

14. SDK 信任建立在本地 runtime process ownership 与完整 event stream 上

TypeScript SDK client 在任何 Harness Context 之外直接 spawn 配置的 runtime。若没有提供显式完整环境,child 继承 process.env;request 与 notification 随后通过私有 JSONL stdio。Request timeout 只放弃 client-side pending entry,不会取消 server work;server-side 工作可能继续或自然完成,而 late response 会被忽略。关闭 owned runtime 才是 controller 强制终止的路径。SEC-SDK-CLIENT

SDK server 订阅其 Context 中每个 Session event 与 Agent status,上报 local child lifecycle,并允许 caller 在 initialize 时选择 cwd、provider 与 model。SEC-SDK-SERVER 把 subscription 过滤到一个 Session tree 是 client-side convenience,不是 server-side authorization。SEC-SDK-FILTER Python client 同样复制完整 parent environment、合并可选 override,并通过 stdio 启动选定 runtime。SEC-SDK-PYTHON

15. ACP 是受信任 automation bridge,只有狭窄的 one-shot approval semantics

ACP module 把自身描述为服务 trusted programmatic client 的 automation-only JSON-RPC stdio server。它创建并拥有 fresh Agent 与 Session,承载 prompt text、committed assistant text、cancellation 与 permission decision,同时把 presentation feature 留在 bridge 之外。SEC-ACP-TRUST

对 approval,bridge 只回答属于自己 Agent 且携带 call ID 的 request。它只提供 allow-once 与 reject-once,把未知非 allow 响应收敛为 rejection;它不广告 authentication method,authenticate 是 no-op;创建 Session 时接受 client 提供的 absolute cwd。SEC-ACP-BRIDGESEC-ACP-CWD

边界

精确 ownership 与 one-shot choice 防止陈旧或外来的 ACP answer 变成 durable grant。它们不认证 stdio peer;安全性取决于哪个 process 启动或连接 bridge 并控制其 pipe。

16. Remote execution 改变 execution world,但不会消除 composition trust

E2B owner 为每个 Context 创建一个共享 sandbox handle,校验 API key、absolute Linux cwd 与 lifetime,向服务请求 secure sandbox,准备 owner-only runtime directory,并随机化 SDK login-shell HOME。Setup failure 后会尝试 rollback kill;timeout 或 dispose 时也会请求 service kill sandbox,但这些 best-effort cleanup 调用并不证明 remote resource 已被销毁。其 API key 明确不会转发进 remote sandbox。SEC-E2B-OWNER

Remote filesystem 不自动与 Host 同步。Mutation coordination 仍只在 Host process 内;其他 connection 或 remote command 可以与 replacement 竞态;打开 canonical path 仍存在 remote reopen race。SEC-E2B-LIMITS

假设

Filesystem、subprocess、shell、terminal 与 search capability 必须全部指向同一个 remote world 与 root。共享该 Context 的 Session 也共享 E2B owner;为 Context 创建一个 remote sandbox 并不证明这些 Session 彼此隔离。

17. Code Mode 与 dynamic plugin 是 cooperative code containment,不是 malicious-code isolation

Worker-thread CodeRuntime 直接说明这一点:fresh worker、空环境、heap limit、busy/wall timer 与 forced termination 属于 containment,而 model code 仍具有 bash-equivalent trust。SEC-WORKER-TRUST

Dynamic Host code 在 fresh node:vm realm 中运行,traps 把常见操作引导到 Services,但 Host-realm helper 仍是 escape route;module 明确声明该 VM 不能 contain malicious code。SEC-DYNAMIC-VM 更关键的是,Host-only dynamic Package 会立即 activate。只有带 Client code 的 Package 才进入 approval request state,而且 plugin-wide grant 可以覆盖未来 Client version。SEC-DYNAMIC-RUN

Host façade 仍是有用的 defense in depth:property-style Service access 要求 declared injection,因此会参与 Cordis lifecycle parking;但 optional ctx.get(name) 不受 declaration gate 限制,可以取得任意 live Service。取得的 Service 会被 wrapper 拦截 direct Context return,但不会深度净化任意 object graph;特殊 tools façade 则只暴露 metadata 与 guarded registration,不暴露 live execute function。SEC-DYNAMIC-GUARD Browser half 通过 page realm 内的 new Function 执行。部分 bare name 被 shadow,process/Buffer 被传入 undefined;但 ambient windowdocumentglobalThis 等 page capability 仍可达,所注入的 React、console、styles 与 Host call 也都是真实 capability。SEC-DYNAMIC-CLIENT

Self-modification toolchain 不属于 base capability set:advanced headless example 显式插入 CodeRuntime、Host runner 与 tool-cordisSEC-SELF-MOD-OPTIN

18. Supply-chain 收口:plugin、skill、MCP server 与 endpoint 最终都会变成 authority

Skill 展示了 syntax safety 与 semantic trust 的区别。模型工具在 lookup 与 load 后都检查 model-invocable,用户显式调用只注入 user-invocable skill,模型 catalog 也只保留 model-invocable 项。SEC-SKILL-INVOCATION-GATES Wrapper 的 name attribute 会 escape;但 instruction body 原样嵌入,因为实现把 skill 归类为 trusted local content。SEC-SKILL-TRUST

Extension 来源控制剩余假设或缺口
Profile plugin / bundlePackage-manager 成功、Loader 校验、事务化 activation在进程内执行;package 与 patch 作者是 administrator
SkillDiscovery parsing 与 invocation policyBody 是受信 model instruction,不做语义 sanitize
MCP serverNamespace、discovery/result-shape checks、timeout、cancellation外部 executable 或 endpoint 拥有真实 side effect
Model provider有边界的 request/model settings、per-request key resolution、transport errorsConfigured endpoint string 接收 prompts、identifiers 与 bearer credential
Dynamic PluginOpt-in toolset、ownership IDs、façades、lifecycle disposalHost VM 可逃逸;Host-only code 无需 approval
判断规则

凡是能新增 plugin、skill、MCP endpoint、model endpoint、approval answerer、sandbox runner 或 dynamic Host Package 的入口,都属于 administrative trust domain,即使配置载体看起来只是 YAML 或 JSON。

19. Control / assumption / gap ledger 与加固优先级

领域现有控制部署假设已知缺口 / 优先级
Remote accessHost、Fetch Metadata、Origin 与 loopback pinningAccepted authority 是受信 controller暴露给不可信网络前增加真正 authentication 与 per-capability authorization
ExecutionTool schema、有序 policy、monotonic guard、one-shot escalation已注册 tool body 受信要求 raw definition 使用显式 validation wrapper;为高影响 dynamic Host execution 单独增加 consent
SandboxFail-closed file-effect runner 与 filesystem containmentOS 或 custom runner 以及 same-world composition 正确缺少 network、credential、process 与 device policy;Windows 仅 partial
SecretsReference-based configuration、owner-only POSIX store、value-free description、child scrubHost process 与配置 endpoint 受信优先用 explicit environment allowlist 或 secret handle 替代名称 heuristic;补充 Windows ACL assurance
Supply chain明确 load failure、explicit mount 与 package-manager policy本地 package、skill 与 server definition 由 administrator 编写若 profile 接纳未 review 来源,增加 provenance/pinning policy
Path 与 concurrencyCanonical identity、containment、atomic publication、version guard不存在同一用户下的敌对 topology manipulatorDescriptor-relative operation 与更强 cross-process coordination 仍是未来工作

本章是对 commit 47f943859bef60e4160492346772ded9b24f765a 的静态 control-flow audit。它区分 implementation control、deployment assumption 与源码声明的 limitation;不声称做过 exploit testing、kernel conformance testing、package provenance verification 或 formal noninterference proof。最安全的操作性解读应保持克制:没有认证就让 Host 留在本地;把每个 administrative extension 当作 code review;只暴露任务真正需要的 capability;把每个配置的 external endpoint 都视为 data-and-authority recipient。

我的学习体会

内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。