Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Agent Store Backend 运行时文档

本文档基于当前 backend/ 源码实现整理,目标是把“今天这套代码到底怎么运行、怎么编排、怎么隔离、怎样保持会话连续性、哪些地方已经实现、哪些地方还需要补齐”说清楚。

文档目标

这本书重点说明以下问题:

  • agent-store-sandbox-backend(云端运行时编排后端)如何接收 session(会话)、artifact(上传文件)、mount grant(挂载授权)和 run(一次运行)
  • agentcore(Agent 核心运行时)与 agent package(开发者提供的 Agent 包)如何组合成一次真实运行
  • 当前隔离粒度到底是“一个用户一个环境”“一个会话一个环境”还是“一个运行一个环境”
  • 容器与宿主机、用户上传文件、本地 Helper 之间如何交换文件
  • 一次运行从 API 请求到 K3s Job(Kubernetes 批处理任务)结束的完整生命周期是什么
  • 同一 session(会话)内的历史、结果文件和可恢复状态如何保持连续
  • 当前代码里哪些能力已经落地,哪些能力还属于下一阶段演进

文档中的统一术语

本文档统一使用下面这组术语:

  • agentcore(Agent 核心运行时):负责执行推理、文件操作、状态恢复、checkpoint、工具调用的运行时执行器。它可以是 Rust 二进制,也可以是镜像内的其它可执行入口。
  • agent package(Agent 包):开发者提供的提示词、工具声明、环境事实、策略和附带资源。
  • RuntimeProfile(运行时配置模板):描述基础镜像、容器入口、资源限制、安装策略和执行模式的模板。
  • SessionRecord(逻辑会话记录):保存同一会话的历史消息、默认包、状态和最近一次运行引用。
  • RunRecord(一次运行记录):保存一次真实运行的工作区、状态、K8s 资源引用和结果索引。

当前代码里的 RuntimeFamily::CodexRuntimeFamily::ClaudeCodecodex-standardclaude-code-standard 这些名字仍会出现在源码和配置里。本文档把它们统一视为样例 runtime family / profile 标识符。从运行时架构角度,真正决定行为的是:

  • 基础镜像 image
  • 容器入口 executable
  • 安装策略 installStrategy
  • 执行模式 executionMode
  • 工作区挂载与资源限制

也就是说,架构并不依赖 agentcore 的实现语言,差异主要体现在基础镜像和容器入口。

一句话结论

当前实现的物理隔离单元是一次 RunRecord(一次运行记录),它会被渲染成一个独立的 K3s Job 和一个独立 Pod;SessionRecord(逻辑会话记录)负责保存多轮历史和会话状态。

当前代码已经保证:

  • 同一 session 内的对话历史连续
  • 已上传文件和历史结果文件长期保留
  • 用户关闭前端或关闭 session 后,后续仍可重新打开同一 session

当前代码还没有把“同一 sessionagentcore checkpoint 目录”做成显式 API 和显式挂载元数据;文档会把这一点作为当前边界和建议方案写清楚。

必须先澄清的三个事实

1. 当前按 run 创建独立容器

同一个 SessionRecord(逻辑会话记录)会跨越多次 create_run(创建运行)。每一轮都会新建一个独立工作区、独立 Job、独立 Pod。前一轮结束后继续复用的是:

  • SessionRecord.history(会话消息历史)
  • ArtifactRecord(上传文件记录)
  • 已落盘的历史结果文件
  • 选定的 agent package
  • 选定的 RuntimeProfile

2. 当前采用“共享运行时模板 + 按 run 注入 package”的模式

RuntimeProfile(运行时配置模板)提供共享的运行时底座;AgentPackageManifest(Agent 包清单)提供本次运行要注入的业务层资源;LaunchPlan(启动计划)把两者与当前工作区、当前 provider 凭据、当前挂载授权拼成一次独立运行。

3. 当前会话连续性主要来自持久化状态

当前代码里,会话连续性主要来自:

  • SessionRecord.history
  • SessionRecord.latest_run_id
  • ArtifactRecord
  • RunRecord
  • backend/data/workspaces/<run-id>/out/ 下的结果文件

如果 agentcore 自带 checkpoint,建议把 checkpoint 写到持久化挂载目录,并由 backend 保存“session_id -> checkpoint_root”的索引;当前代码还没有把这块做成显式 API。

代码入口

如果要从源码反向阅读整个流程,建议按下面顺序看:

  1. backend/src/main.rs
  2. backend/src/lib.rs
  3. backend/src/api/mod.rs
  4. backend/src/service.rs
  5. backend/src/k8s/mod.rs
  6. backend/src/k8s/job_builder.rs
  7. backend/src/storage/mod.rs
  8. backend/src/provider.rs
  9. backend/src/package/mod.rs

其中有两个 K8s 相关入口需要先记住:

  • backend/src/k8s/mod.rs:通过 kube(Rust Kubernetes 客户端)执行 namespace、service account、job、pod、node 相关的读写和状态检查。
  • backend/src/k8s/job_builder.rs:通过 k8s_openapi(Rust Kubernetes 资源类型定义)在内存里构造 ConfigMapSecretJob,再渲染成 YAML 或提交到集群。

接下来的章节会按这个顺序拆开讲。

安装与本地预览

这一章只说明如何把当前项目跑起来,以及如何预览本书本身。

1. Rust Workspace

仓库根目录是一个 Cargo workspace,包含两个成员:

  • backend/
  • macos-helper/

在仓库根执行:

cargo test --workspace

如果需要更严格的静态检查:

cargo clippy --workspace --tests -- -D warnings

2. 启动云端后端

2.1 本地 dry-run 预览模式

这种模式不会真的调用 Kubernetes API,只会把即将提交的资源渲染出来。

export AGENT_STORE_BACKEND_CLUSTER_MODE=dry-run
cargo run -p agent-store-sandbox-backend

2.2 真实 K3s 模式

这种模式会调用 K3s,把每次运行提交成 Job

export AGENT_STORE_BACKEND_BIND_ADDR=0.0.0.0:8090
export AGENT_STORE_BACKEND_CLUSTER_MODE=apply
export AGENT_STORE_BACKEND_DEFAULT_NAMESPACE=agent-store-runtime-demo
export AGENT_STORE_BACKEND_DEFAULT_SERVICE_ACCOUNT=agent-runner
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

export DEEPSEEK_API_KEY=replace-me
export DEEPSEEK_MODEL=deepseek-chat
export DEEPSEEK_BASE_URL=https://api.deepseek.com/v1

cargo run -p agent-store-sandbox-backend

2.3 远程部署

当前仓库已经提供了部署模板:

  • backend/deploy/backend.env.example:环境变量模板
  • backend/deploy/agent-store-backend.service:systemd 单元模板
  • backend/deploy/install_backend_remote.sh:一键远程安装脚本

3. 启动本地 Helper

agent-store-macos-helper(macOS 本地桥接服务)负责把本地工作目录同步成云端 artifact,并把远端结果回写到本地。

export AGENT_STORE_BACKEND_BASE_URL=http://127.0.0.1:8090
export AGENT_STORE_USER_ID=demo-user
export MACOS_HELPER_BIND_ADDR=127.0.0.1:7480
export MACOS_HELPER_POLL_TIMEOUT_MS=120000

cargo run -p agent-store-macos-helper

浏览器访问:

http://127.0.0.1:7480

4. 打包 Helper

bash macos-helper/scripts/package_macos_helper.sh

输出文件:

  • macos-helper/dist/Agent Store Helper.app
  • macos-helper/dist/Agent Store Helper.zip

5. 构建本书

本书采用 mdBook 目录结构。首次安装工具:

cargo install mdbook --locked

构建 HTML:

mdbook build backend/docs

本地预览(本机装了 mdbook 时会启用热重载;否则回退到直接托管已构建好的 backend/docs/book/):

bash backend/scripts/serve_docs.sh

访问:

http://127.0.0.1:3000

如果要把文档跑在远端机器或沙箱并允许任意位置访问,且远端已经有 backend/docs/book/index.html

DOCS_SERVE_MODE=static MDBOOK_HOST=0.0.0.0 MDBOOK_PORT=3452 \
bash backend/scripts/serve_docs.sh

浏览器访问:

http://<remote-host>:3452

6. 快速回归命令

后端附带一个 smoke 脚本:

BACKEND_BASE_URL=http://127.0.0.1:8090 \
AGENT_STORE_USER_ID=smoke-user \
bash backend/scripts/smoke_backend_run.sh

如果只想看资源渲染,把环境变量 AGENT_STORE_APPLY_TO_CLUSTER=false 加进去即可。

运行时总体架构

这一章解释 backend/ 里“谁负责什么”,以及一次运行到底由哪些对象拼出来。

1. 启动时会装配哪些核心组件

build_application(应用装配入口)在启动时会把下面这些组件接起来:

代码对象当前职责
AppConfig(进程级配置)读取 HTTP 监听地址、存储根目录、包目录、运行时配置、集群模式、默认命名空间、默认服务账号、挂载白名单、Job TTL
FileBackedStore(文件型状态存储)维护 state.jsonuploads/workspaces/,保存 sessionartifactmount grantrun
PackageCatalog(Agent 包目录索引)backend/packages/* 加载成内存里的 LoadedPackage(已加载 Agent 包)
RuntimeProfileCatalog(运行时模板目录索引)backend/config/runtime_profiles.yaml 加载成内存里的 RuntimeProfile
K3sOrchestrator(K3s 编排器)渲染和可选提交 ConfigMapSecretJob,并观察 K8s 状态
ApplicationService(应用服务层)串起 API、状态存储、工作区准备、K8s 提交、状态刷新、多轮会话同步
Router(HTTP 路由)对外暴露 /api/v1/* 接口

这意味着当前后端是一个“单进程控制面 + K3s 执行面”的结构:

  • API 请求先进入 Router(HTTP 路由)
  • RouterApplicationService(应用服务层)
  • ApplicationService 同时依赖本地持久化状态和 K3sOrchestrator(K3s 编排器)
  • 真正执行 agentcore 的单元是被后端提交出去的 K3s Job

2. 当前系统里最重要的几个对象

2.1 SessionRecord(逻辑会话记录)

SessionRecord 表示“同一用户的一段逻辑会话”。核心字段包括:

  • user_id
  • default_package
  • status
  • history
  • latest_run_id
  • closed_at

它的职责是保存多轮对话时间线,容器实例由后续的 run 单独创建。

2.2 ArtifactRecord(上传文件记录)

ArtifactRecord 表示用户上传到 backend 的文件元数据。文件本体会先落到 uploads/,后续在创建 run 时复制进当前 run 的工作区。

2.3 MountGrantRecord(宿主机挂载授权记录)

MountGrantRecord 表示“允许某个用户把某个 backend 所在主机目录挂进容器”。它是显式授权对象,包含:

  • host_path
  • mount_path
  • access_mode

2.4 RunRecord(一次运行记录)

RunRecord 描述一次真实运行,包含:

  • 所属会话 session_id
  • 使用的包 package_name
  • 使用的运行时模板 runtime_profile
  • 工作区模式 workspace_mode
  • 输入文件 artifact_ids
  • 可选挂载授权 mount_grant_id
  • provider 环境变量键名 provider_env_keys
  • 工作区根目录 workspace_root
  • 运行状态 status
  • K8s 资源引用 resources
  • 渲染结果 rendered

当前物理隔离单元就是 RunRecord

2.5 RuntimeProfile(运行时配置模板)

RuntimeProfile 决定一次 run 的基础运行时底座,主要包括:

  • 基础镜像 image
  • 容器入口 executable
  • 安装策略 installStrategy
  • 执行模式 executionMode
  • 默认模型和默认 Base URL
  • namespace、service account
  • CPU、内存、临时存储的 requests / limits

从运行时设计上看,RuntimeProfile 表达的是“怎样启动一个 agentcore 容器”,并不绑定实现语言。

2.6 AgentPackageManifest(Agent 包清单)

AgentPackageManifest 定义 agent package 本身的业务层意图,主要包括:

  • runtime
  • entry
  • policy
  • assets

当前包目录示例位于:

  • backend/packages/codex-reviewer/
  • backend/packages/claude-code-builder/

这些目录名是样例包名,架构层仍统一按 agent package 理解。

3. agentcoreagent package 当前怎样组合

3.1 agentcore 在当前代码里对应什么

如果把 agentcore 理解为“真正提供执行底座的运行时”,那在当前代码里它主要对应三部分:

  • RuntimeProfile(运行时配置模板)
  • 运行时镜像 image
  • 容器入口 executable

它们以共享模板的方式存在。

3.2 agent package 在当前代码里对应什么

如果把 agent package 理解为“开发者提供的提示词、工具声明、环境事实和策略”,那它对应:

  • agent.yaml
  • prompt/system.md
  • build_in_tools.yaml
  • env/env_facts.yaml
  • 其它包内文本文件

这些文件在运行时由后端加载成 LoadedPackage(已加载 Agent 包),再通过 ConfigMap 注入 Pod 内的 /opt/agent/package

3.3 两者的绑定时机

LaunchPlan(一次运行的不可变启动计划)是 agentcoreagent package 真正结合的地方。它同时持有:

  • package
  • runtime_profile
  • workspace_mode
  • provider_env
  • mount_grant
  • workspace_host_path
  • namespace
  • service_account

随后 K3sOrchestrator 会把这个 LaunchPlan 渲染成三类资源:

  1. ConfigMap
  2. Secret
  3. Job

3.4 当前组合模式的结论

结论很明确:

  • 共享的是 RuntimeProfile 和基础镜像
  • 独立的是每次运行的 Pod、工作区、ConfigMapSecret
  • 一次 run 会把“共享运行时模板 + 当前 agent package + 当前工作区 + 当前 provider 凭据”拼成一个独立运行单元

4. 当前隔离模型与连续性模型

4.1 逻辑隔离按 session

SessionRecord 保存:

  • 历史消息
  • 默认包
  • 会话状态
  • 最近一次 run

它定义的是“同一段对话时间线”。

4.2 物理隔离按 run

每次 create_run(创建运行)都会创建:

  • 一个独立工作区目录
  • 一个独立 ConfigMap
  • 一个可选独立 Secret
  • 一个独立 K3s Job
  • 一个独立 Pod

这意味着当前模型是:

  • 逻辑连续性按 session
  • 物理执行隔离按 run

4.3 会话连续性今天靠什么保持

当前代码里的连续性主要来自三块持久化数据:

  1. SessionRecord.history(会话消息历史)
  2. ArtifactRecord(上传文件记录)和 uploads/ 下的原始文件
  3. RunRecord(运行记录)和 workspaces/<run-id>/out/ 下的结果文件

ApplicationService::sync_session_conversation_history(同步会话历史)会在新 run 开始前刷新该 session 下旧 run 的状态;ApplicationService::sync_run_assistant_message(同步助手回答)会把已完成 run 的 final-answer.md 回填进 SessionRecord.history。这样同一 session 的下一轮可以拿到前一轮回答。

4.4 agentcore 的 checkpoint 连续性应该怎样接入

从通用运行时架构看,agentcore 的 checkpoint 连续性应该依赖持久化挂载路径,典型模式是:

  • 为每个 session 分配一个持久化状态目录
  • 在同一 session 的每次 run 中都把这个目录挂到固定路径
  • agentcore 在该目录内保存 checkpoint、状态数据库、工具缓存和恢复索引
  • 由 backend 保存 session_id -> checkpoint_root 的索引

当前代码已经具备:

  • run 级工作区持久化
  • session / run 元数据持久化
  • reopen 同一 session

当前代码还没有显式的:

  • checkpoint_root 字段
  • session 级持久化状态卷
  • resume API

这部分会在“当前实现边界”章节里继续说明。

5. K8s Rust 调用在当前架构中的位置

这一节专门把代码和功能对应起来。

5.1 backend/src/k8s/mod.rs 负责真正访问集群

K3sOrchestrator::new(K3s 编排器初始化)通过 Client::try_default() 创建 kube 客户端,并调用:

  • ensure_namespace:确保 namespace 存在
  • ensure_service_account:确保 service account 存在且关闭自动挂载 token

K3sOrchestrator::submit_run(提交运行)负责:

  • job_builder::build_bundle 在内存里构造 ConfigMapSecretJob
  • apply 模式下调用 Api<ConfigMap>::create
  • 调用 Api<Secret>::create
  • 调用 Api<Job>::create

K3sOrchestrator::inspect_run_status(检查运行状态)负责:

  • Api<Job>::get_opt 读取 Job 状态
  • Api<Pod>::list 读取同一 Job 对应 Pod 的状态
  • 把 Job / Pod 观察结果映射成 RunStatus

K3sOrchestrator::cluster_status(读取集群状态)负责:

  • Api<Node>::list 读取节点
  • 汇总可调度节点数、taint、磁盘压力和阻塞原因

K3sOrchestrator::cancel_run(取消运行)负责:

  • Api<Job>::delete
  • Api<ConfigMap>::delete
  • Api<Secret>::delete

5.2 backend/src/k8s/job_builder.rs 负责把计划渲染成资源

job_builder::build_bundle(构建运行资源包)会:

  1. 构建承载 package 文件的 ConfigMap
  2. 构建承载 provider 凭据的 Secret
  3. 构建真正执行 run 的 Job

build_job(构建 Job)会把下面这些设置落到资源定义里:

  • /workspace 工作区挂载
  • /opt/agent/package 包目录挂载
  • 可选的用户授权挂载
  • allowPrivilegeEscalation: false
  • capabilities.drop = ["ALL"]
  • runAsNonRoot = true
  • fsGroup = 1000
  • seccompProfile = RuntimeDefault
  • ttl_seconds_after_finished

5.3 backend/src/service.rs 负责把业务对象和 K8s 编排串起来

ApplicationService::create_run(创建运行)是控制流入口,它会:

  1. 校验 session
  2. 解析 package
  3. 解析 RuntimeProfile
  4. 合并 provider 配置
  5. 校验输入文件和挂载
  6. 做集群预检
  7. 同步历史回答
  8. 追加本轮用户消息
  9. 创建工作区
  10. run-context.json
  11. 组装 LaunchPlan
  12. K3sOrchestrator::submit_run
  13. 持久化 RunRecord

6. 当前样例 runtime profile 如何理解

runtime_profiles.yaml 里当前有多组样例 profile。它们在代码里沿用了历史命名,例如:

  • codex-standard
  • claude-code-standard
  • codex-cli-standard
  • claude-code-cli-standard

这些名字只代表当前仓库里已有的样例配置。架构层需要关注的是下面这些字段:

  • image
  • executable
  • installStrategy
  • executionMode
  • requests
  • limits

因此如果后续接入新的 Rust agentcore、Python agentcore 或其它容器入口,后端运行时模型本身不需要变化,变化点主要是:

  • 基础镜像
  • 容器入口
  • checkpoint 目录约定
  • 执行模式适配器

7. backend 与 agentcore 的责任边界

当前更合理的责任边界是:

  • agentcore 负责自己的执行逻辑、checkpoint、工作目录状态、工具调用和恢复逻辑
  • agent package 负责提示词、工具声明、环境事实和策略
  • backend 负责保存 sessionrun、上传文件、工作区目录、K8s 资源引用、结果文件索引以及 reopen/resume 入口

这条边界与 agentcore 的实现语言无关。

隔离模型、权限模型与安全边界

这一章专门回答“隔离做到哪一层”“权限到底有多强”“哪些能力是声明性元数据,哪些能力已经强制生效”。

1. 隔离粒度:当前分成三层

当前代码里实际上存在三种不同层次的隔离。

1.1 user_id(用户所有权边界)是对象级隔离

后端要求所有用户态 API 都带 x-user-idFileBackedStore(文件型状态存储)在读取 SessionRecordArtifactRecordMountGrantRecordRunRecord 时都会调用 ensure_owner(所有者校验)。

这意味着:

  • 一个用户看不到另一个用户的 session / run / artifact / mount grant
  • 即使知道 UUID,跨用户读取也会返回 “not found”

这是一层应用层所有权隔离。当前代码没有扩展成完整身份认证体系。

1.2 SessionRecord(逻辑会话记录)是上下文隔离层

一个 SessionRecord 拥有自己的:

  • history
  • 默认 package
  • 关闭状态

它能保证多轮对话上下文按 session 归组。一个 session 可以连续创建多个 run,这些 run 共享逻辑历史,容器实例按 run 单独创建。

1.3 RunRecord(一次运行记录)是物理执行隔离

每次 create_run 都会创建:

  • 一个独立工作区目录
  • 一个独立 ConfigMap
  • 一个可选独立 Secret
  • 一个独立 K3s Job
  • 一个独立 Pod

所以当前最强的实际隔离边界是:

一次 run = 一个独立容器执行环境

2. 当前隔离强度怎么理解

如果用“隔离强度”来拆,当前可以分成四类。

2.1 进程与文件系统隔离:中等偏强

每个 run 都在独立 Pod 中运行,工作区也是独立目录,默认路径是:

  • 宿主机:backend/data/workspaces/<run-id>/
  • 容器内:/workspace

因此不同 run 的输出文件天然分目录隔离。

当前工作区挂载方式是 hostPath(宿主机目录直挂)。当前隔离强度属于较轻量的容器级隔离;如果后续要提升隔离,可以继续引入 PVC、CSI 或更强沙箱方案。

2.2 Linux 权限隔离:中等

Job 里的容器安全上下文当前明确设置了:

  • allowPrivilegeEscalation: false
  • capabilities.drop = ["ALL"]
  • runAsNonRoot: true
  • runAsUser: 1000
  • runAsGroup: 1000
  • fsGroup: 1000
  • seccompProfile.type = RuntimeDefault
  • automountServiceAccountToken: false
  • restartPolicy: Never

这说明当前 Pod 默认不会拿到:

  • root 身份
  • Linux capabilities
  • 自动挂载的 K8s API token

这部分是代码里真实生效的安全边界。

2.3 网络隔离:弱

AgentPackageManifest(Agent 包清单)里虽然有 networkProfile(网络档位),例如 restricted-egress,但当前 backend 不会依据这个字段创建 NetworkPolicy

  • networkProfile 现在只是包元数据和提示信息
  • 真正能否访问外网,取决于集群网络、节点出网能力、镜像内工具和 provider API 可达性

所以当前网络隔离强度偏弱,不能把 networkProfile 当成已落地的强制策略。

2.4 能力权限隔离:部分生效,部分只是声明

PackagePolicy(Agent 包策略)里的两个字段要分开看:

  • workspaceMode:真实生效,会影响输入文件是 copy 还是 mount
  • permissionProfile:当前只是一段包策略元数据,没有被后端强制映射成 Linux/K8s/工具权限

所以目前真正“强制落地”的权限主要来自:

  • Pod 安全上下文
  • 工作区 copy / mount 选择
  • 挂载白名单
  • readOnly / readWrite 挂载模式

permissionProfile 当前承担的是描述和标识作用。

3. 一个 session、一个用户、一个 run,分别隔离什么

隔离单元当前是否独立容器当前是否独立工作区当前是否独立历史当前用途
用户作为所有权边界
Session作为多轮上下文边界
Run只记录本轮作为实际执行边界

因此如果要回答“目前是一个会话一个隔离环境,还是一个用户一个隔离环境”,答案是:

  • 逻辑隔离按 session
  • 物理隔离按 run

4. copymount 两种工作区模式的安全差异

4.1 WorkspaceMode::Copy(复制模式)

复制模式下,后端会把 ArtifactRecord 对应的文件从 uploads/ 复制到当前 run 的 /workspace/in

优点:

  • 每次 run 的输入快照固定
  • 容器只看到本轮复制进去的文件
  • 不会直接暴露宿主机原始目录树

代价:

  • 大目录会产生重复拷贝
  • 用户本地后续改动不会自动映射到已提交的 run

4.2 WorkspaceMode::Mount(挂载模式)

挂载模式下,必须先创建 MountGrantRecord。后端会把 grant 的 host_path 作为额外 hostPath volume 挂到容器内的 mount_path

优点:

  • 不需要复制大目录
  • 容器看到的是宿主机目录的实时内容

风险:

  • 隔离性明显弱于 copy
  • 一旦授权 readWrite,容器就可以直接修改宿主机目录

因此 mount 模式本质上是“受控直通”能力。

5. 挂载授权当前怎么控

validate_mount_path(挂载路径校验)会做三件事:

  1. 把用户给的路径 canonicalize
  2. 确认它是一个已存在目录
  3. 确认它落在 AGENT_STORE_BACKEND_ALLOWED_MOUNT_ROOTS 白名单内

默认白名单是:

backend/data/workspaces

这说明当前 mount grant 的默认设计目标是:

  • 给 backend 自己管理的目录做受控回挂
  • 默认不开放整个宿主机文件系统

6. 容器内到底能访问什么文件

当前容器内常见文件可分成四类。

6.1 始终存在的工作区

  • /workspace/in
  • /workspace/out
  • /workspace/tmp

这三者都来自 run 对应的宿主机工作区目录。

6.2 始终存在的 package 目录

  • /opt/agent/package

这里挂的是 ConfigMap,里面是 package 的文本资产。

6.3 可选的 provider 凭据

provider 凭据不会作为文件卷挂进去,而是通过 envFrom.secretRef 注入环境变量。

6.4 可选的用户授权挂载

如果本次 run 绑定了 MountGrantRecord,还会额外挂一个 user-mount volume 到指定路径。

7. 容器如何访问外部工具、宿主机工具和本地工具

7.1 容器内可以访问哪些“外部工具”

当前真正可执行的工具只有三类:

  • 基础镜像里自带的工具,例如 bashnode
  • inlineNpm(运行时内联 npm 安装)装出来的 CLI
  • 容器内通过 HTTP 访问的模型 API

7.2 build_in_tools.yaml 当前是提示词工具清单

build_in_tools.yaml(内置工具清单)虽然声明了 builtin.readbuiltin.writebuiltin.ask_user 等工具,但当前实现只是把它们写进 run-context.json 供模型参考。

后端目前没有

  • 独立的 tool call RPC 通道
  • tool execution dispatcher
  • tool result callback 回路

当前实现里,这一层承担的是提示词上下文拼装职责。

7.3 容器不能直接访问 backend 主机任意工具

当前没有实现:

  • 宿主机命令代理
  • SSH proxy 执行
  • helper 本地工具转发
  • 本地 IDE / Git / shell 代理

除非某个宿主机目录被显式 mount 进来,否则容器看到的只是:

  • 镜像内容
  • 工作区内容
  • package 内容
  • 环境变量

7.4 容器也不能直接访问 macOS 本地工具

macos-helper 当前承担的是“文件同步桥”职责。它会把本地文件上传成 artifact,当前没有把 macOS 上的 shell、git、编辑器或 CLI 映射成远端容器可调用工具。

7.5 支持本地工具调用的可行方案

如果后续要让远端 agentcore 访问本地工具,当前最可行的方案有三类:

  • macos-helper(macOS 本地桥接服务)增加本地工具 RPC 层,由 helper 执行 Git、shell、编辑器等受控工具
  • helper 与 backend 之间建立长连接工具隧道,由 backend 转发工具调用请求
  • 继续保持当前文件同步模式,把本地工具执行放在 helper 侧完成,再把结果同步成文件或补丁

无论采用哪种方案,都需要补齐:

  • 工具白名单
  • 参数校验
  • 会话级认证
  • 审批模型
  • 超时与取消
  • 审计日志

7.6 当前未实现点

当前代码还没有:

  • 远端容器到本地 helper 的工具调用协议
  • helper 侧的受控命令执行器
  • 本地工具的权限和审批模型
  • 本地工具执行结果回写到 agentcore 的标准事件流

8. provider 凭据隔离如何做

ProviderDefaults(provider 默认值合并器)会合并三层来源:

  1. backend 进程环境变量
  2. RuntimeProfile 里的默认 baseUrl / model
  3. 单次请求的 provider_env

合并后得到的环境变量会被写成每 run 独立的 K8s Secret。渲染返回给前端时,providerSecretYaml 里的值会被替换成 <redacted>

这说明当前 provider 凭据隔离粒度是:

  • 按 run 注入
  • 按 Secret 隔离
  • 按用户 API 读取权限隔离

9. 当前安全边界的真实结论

如果只基于代码,不做理想化表述,可以下结论:

  1. 当前已经有“每 run 独立 Pod + 非 root + drop capabilities + seccomp + 禁止自动 SA token”这一层容器安全基线。
  2. 当前默认更安全的输入方式是 copymount 属于更强权限能力。
  3. 当前 mount 具备白名单和只读/读写控制,但仍然属于较强权限能力,应谨慎开放。
  4. 当前 permissionProfilenetworkProfile 主要还是描述性元数据,不应误认为强制安全策略。
  5. 当前没有把本地 macOS 工具或 backend 宿主机工具安全地暴露给容器的机制。

文件管理、挂载与数据流

这一章专门描述“文件从哪里来、到哪里去、run 结束后谁负责保留这些文件”,以及同一 session 内文件与会话怎样保持连续。

1. backend 本地磁盘的目录布局

FileBackedStore(文件型状态存储)在 storage_root 下维护三类核心目录和一个状态文件:

backend/data/
├── state.json
├── uploads/
└── workspaces/
    └── <run-id>/
        ├── in/
        ├── out/
        └── tmp/

含义如下:

  • state.json:保存 SessionRecordArtifactRecordMountGrantRecordRunRecord
  • uploads/:保存用户上传的原始文件副本
  • workspaces/<run-id>/in/:本轮输入
  • workspaces/<run-id>/out/:本轮输出
  • workspaces/<run-id>/tmp/:本轮临时文件

这意味着当前 backend 采用的是本地持久化控制面

2. K8s 和 backend 分别保留什么

这部分需要先分清楚。

  • SessionRecord.history(会话历史)、ArtifactRecord(文件记录)、RunRecord(运行记录)由 backend 保存在 state.json
  • uploads/workspaces/ 由 backend 所在主机磁盘保存
  • JobPodConfigMapSecret 由 K8s 管理
  • checkpoint 文件或状态数据库如果写在挂载卷内,就跟随宿主机目录或 PVC 持久化

因此:

  • 对话历史不保存在 K8s
  • K8s 负责容器对象生命周期
  • backend 负责会话、文件索引和结果索引
  • checkpoint 是否可恢复,取决于 agentcore 把它写到哪里

3. 用户上传文件怎样进入系统

3.1 HTTP 上传入口

上传接口是:

POST /api/v1/artifacts/upload

它使用 multipart/form-data,当前识别的字段有:

  • sessionId
  • file
  • relativePath

3.2 backend 落盘逻辑

ApplicationService::upload_artifact_bytes(上传文件字节流)会先校验 session 是否可写,然后调用 FileBackedStore::save_artifact_bytes(保存文件字节流)。后者会:

  1. 清洗文件名
  2. 清洗相对路径
  3. 把文件写到 uploads/
  4. state.json 里写入 ArtifactRecord

3.3 relative_path 的意义

relative_path 不会影响原始上传文件在 uploads/ 的落盘位置,但会影响后续 create_workspace(创建工作区)时复制进 /workspace/in 的目录结构。

4. create_workspace 到底做了什么

FileBackedStore::create_workspace(创建工作区)是文件流的核心步骤之一。

4.1 它会创建固定三层目录

<workspace-root>/
├── in/
├── out/
└── tmp/

4.2 它会把 artifact 复制进 in/

  • 如果 artifact 有 relative_path,按相对目录复制
  • 否则落成 <artifact-id>-<sanitized-file-name>

4.3 它会设置宽松目录权限

当前 Unix 上会把工作区目录权限设成 0o777,目的是避免 backend 进程创建目录后,容器内 UID 1000 无法访问。这个做法解决了当前单机联调中的权限问题,后续如果要提高隔离强度,应改成更细粒度的 owner / group 管理。

5. 容器怎样拿到这些文件

job_builder::build_job(构建 Job)会把宿主机工作区通过 hostPath 挂到容器内的 /workspace。因此当前路径关系是:

用户文件
  -> 上传到 backend /uploads
  -> create_run 时复制到 /workspaces/<run-id>/in
  -> 通过 hostPath 挂到容器 /workspace/in

同一个 Job 还会把 agent package 通过 ConfigMap 挂到:

/opt/agent/package

如果本次运行绑定了 MountGrantRecord(挂载授权记录),还会额外挂一个用户授权目录。

6. run-context.json 怎样生成

在提交 Job 前,ApplicationService::write_run_context(写入运行上下文)会把一个关键文件写到:

/workspace/in/run-context.json

它包含:

  • sessionId
  • latestRunId
  • history
  • package
  • builtInTools
  • envFacts
  • inputArtifacts
  • systemPrompt
  • userPrompt

容器里的 agentcore 会读取这个上下文文件获取本轮所需信息。

7. 会话连续性与文件连续性

这一节从“用户关掉页面后,下次回来还能不能接着聊”这个角度解释当前代码。

7.1 当前已经保证的连续性

当前代码已经保证三类连续性:

  1. 对话历史连续
    SessionRecord.history 会持久化保存。
  2. 上传文件连续
    ArtifactRecorduploads/ 下的原始文件会保留。
  3. 历史结果文件连续
    RunRecordworkspaces/<run-id>/out/ 会保留。

7.2 一轮 run 完成后,历史怎样进入下一轮

ApplicationService::sync_session_conversation_history(同步会话历史)会在新 run 创建前刷新同一 session 下旧 run 的状态。

ApplicationService::sync_run_assistant_message(同步助手回答)会在某个 run 完成后读取:

workspaces/<run-id>/out/final-answer.md

然后把其中内容追加为一条 assistant 消息写入 SessionRecord.history

因此下一次同一 session 再创建 run 时,run-context.json 里已经包含前一轮回答。

7.3 用户关闭前端,再次打开同一 session

如果用户只是关闭前端页面、退出 Helper 或暂时离线,而没有调用 POST /api/v1/sessions/{id}/close,那下面这些内容都会保留:

  • SessionRecord.history
  • SessionRecord.latest_run_id
  • 已上传的 artifact
  • 历史 RunRecord
  • 历史 run 的 out/ 结果文件

这时前端只需要重新使用原来的 session_id 调用:

  • GET /api/v1/sessions/{id}
  • GET /api/v1/runs

随后继续 POST /api/v1/runs 即可。

7.4 session 被关闭后,再回来继续

如果调用了 POST /api/v1/sessions/{id}/close,backend 会把 SessionRecord.status 设为 CLOSED。当前行为是:

  • 历史记录保留
  • artifacts 保留
  • runs 保留
  • 结果文件保留
  • 新的上传、挂载授权和 run 会被拒绝

如果之后还要继续同一会话,需要先调用:

POST /api/v1/sessions/{id}/reopen

reopen 只会改变 session 的可写状态,不会删除任何历史数据。因此一个月后再打开相同 session,历史和结果文件仍然在。

7.5 目前“连续”的边界在哪里

当前代码已经连续保存:

  • 文本对话历史
  • 上传文件
  • 历史结果文件

当前代码还没有自动连续保存:

  • 同一 session 的共享工作目录
  • 同一 sessioncheckpoint_root
  • 上一轮输出文件自动进入下一轮输入

因此今天这套代码能保证“会话历史连续”和“结果文件可追溯”,但“agentcore 内部 checkpoint 的自动恢复”还需要显式的 session 级状态目录设计。

8. checkpoint 文件或状态数据库应写到哪里

如果 agentcore 需要保存:

  • checkpoint 文件
  • SQLite / RocksDB / LMDB 这类状态数据库
  • 工具缓存
  • 中间索引

这些数据都应写到持久化挂载目录中。当前最直接的路径是 /workspace 下的子目录,例如:

/workspace/state/
/workspace/checkpoints/
/workspace/checkpoint-db/

如果它们只写在容器根文件系统里,Pod 删除后数据就一起消失。

9. run 结束后,K8s 如何处理文件、对话历史和 checkpoint

这是当前文档必须说清楚的一点。

9.1 K8s 负责的部分

K8s 负责:

  • Pod 结束
  • Job 结束
  • Job TTL 到期后的 Job / Pod 回收
  • 显式 cancel 时的资源删除

9.2 backend 负责的部分

backend 负责:

  • state.json 中的 sessionartifactrun 元数据
  • uploads/ 中的原始上传文件
  • workspaces/<run-id>/out/ 中的结果文件

9.3 ConfigMap / Secret 的当前处理

当前完成态 run 的 ConfigMapSecret 还没有统一的后台 GC。取消 run 时会显式删除;正常完成后主要依赖后续清理策略补齐。

9.4 checkpoint 的当前处理语义

K8s 本身不理解 checkpoint 语义,它只知道卷挂载和容器对象。真正决定 checkpoint 是否保留的是落盘位置

  • checkpoint 写在容器根文件系统:Pod 删除后丢失
  • checkpoint 写在挂载卷:Pod 删除后仍保留

当前代码没有独立的 checkpoint 路径索引和恢复 API,所以这部分连续性仍取决于 agentcore 的落盘约定和后续的 session 级状态卷设计。

10. 如果要支持“同一 session 长期继续运行”,推荐的数据模型

从通用运行时设计看,更完整的模型是:

backend/data/
├── state.json
├── uploads/
├── workspaces/<run-id>/
└── session-state/<session-id>/
    ├── checkpoints/
    ├── checkpoint-db/
    └── tool-cache/

推荐流程是:

  1. 第一次 run 时,为 session_id 创建 session-state/<session-id>/
  2. 后续同一 session 的每次 run 都把该目录挂到固定路径
  3. agentcore 把 checkpoint 和状态数据库始终写在这个固定路径
  4. backend 在 SessionRecord 或新的 SessionStateRecord 中保存该路径索引

这样可以保证:

  • 文本历史连续
  • 文件结果连续
  • agentcore checkpoint 也连续

当前代码离这一步还差显式元数据和显式挂载规则。

运行生命周期与容器编排流程

这一章从用户视角出发,解释当前代码里一次会话、一次运行和一次容器编排到底怎样流动。

1. 从用户视角看,一次会话会经历什么

用户在产品里看到的主流程可以理解为下面几步:

  1. 打开一个 session(会话)
  2. 上传文件,或选择已有文件
  3. 选择一个 agent package
  4. 发送一轮 prompt
  5. backend 为这轮 prompt 创建一个新的 run
  6. K3s 为这个 run 拉起一个独立 Pod
  7. agentcore 在 Pod 里处理本轮任务并写结果
  8. backend 把结果状态写回 run,把主回答写回 session history
  9. 用户离开页面,稍后回来继续使用同一个 session

这个流程里,“连续的是 session”,“独立的是 run”。

2. 后端进程生命周期

main(进程入口)启动后会依次完成:

  1. 初始化 tracing
  2. 兼容旧环境变量别名
  3. 解析 AppConfig(进程级配置)
  4. build_application(应用装配入口)
  5. 绑定 HTTP 地址
  6. 用 axum 对外提供 API
  7. 监听退出信号并优雅关闭

如果 backend 运行在 Apply 模式,K3sOrchestrator::new(K3s 编排器初始化)还会确保:

  • namespace 存在
  • service account 存在
  • service account 关闭自动挂载 token

2.1 启动伪代码

async fn main() {
    init_tracing(); // 初始化日志
    apply_legacy_env_aliases(); // 兼容旧环境变量

    let config = AppConfig::parse(); // 解析进程配置
    let app = build_application(config.clone()).await?; // 装配 store/catalog/orchestrator/router
    let listener = TcpListener::bind(config.bind_addr).await?; // 绑定 HTTP 地址

    axum::serve(listener, app.router)
        .with_graceful_shutdown(shutdown_signal())
        .await?;
}

3. session 的生命周期

SessionRecord(逻辑会话记录)当前只有两个状态:

  • ACTIVE
  • CLOSED

生命周期如下:

  1. POST /api/v1/sessions 创建,初始为 ACTIVE
  2. 可反复用于多轮 create_run
  3. POST /api/v1/sessions/{id}/close 后变成 CLOSED
  4. POST /api/v1/sessions/{id}/reopen 后回到 ACTIVE
  5. CLOSED 状态下仍可查询历史和结果

这里要特别注意:close 表示“归档并拒绝新写入”,会话数据会继续保留。

4. run 的生命周期

RunRecord(一次运行记录)常见状态包括:

  • PREVIEWED
  • SUBMITTED
  • PENDING
  • COMPLETED
  • FAILED
  • CANCELLED

其生命周期是:

  1. 用户请求创建 run
  2. backend 准备工作区和上下文
  3. backend 提交或渲染 K3s 资源
  4. Pod 执行
  5. backend 刷新状态
  6. run 进入完成、失败或取消态

5. create_run 的完整用户视角流程

5.1 用户看到的过程

从用户角度,一次“发送消息”背后发生的是:

  1. backend 读取当前会话
  2. backend 读取本轮所选文件和挂载授权
  3. backend 检查集群是否还能接收新任务
  4. backend 把旧回答补回会话历史
  5. backend 把本轮用户输入追加到会话历史
  6. backend 创建本轮独立工作区
  7. backend 把上下文写成 run-context.json
  8. backend 构造 ConfigMapSecretJob
  9. K3s 启动本轮 Pod
  10. Pod 把本轮结果写回工作区

5.2 当前代码的伪代码

#![allow(unused)]
fn main() {
async fn create_run(command: CreateRunCommand) -> RunRecord {
    let run_id = new_uuid_v7(); // 生成本轮唯一 id

    let session = store.get_session(command.user_id, command.session_id).await?; // 读取会话
    ensure_session_accepts_writes(session)?; // 关闭态会话拒绝新 run

    let package = packages.get(command.package_name)?; // 读取 agent package
    let runtime_profile = resolve_runtime_profile(command, package)?; // 解析运行时模板
    let provider_env = merge_provider_env(runtime_profile, command.provider_env); // 合并 provider 配置

    let artifacts = store.get_artifacts(command.user_id, command.artifact_ids).await?; // 读取输入文件
    let mount_grant = resolve_mount_grant(command.mount_grant_id).await?; // 读取可选挂载授权
    let workspace_mode = resolve_workspace_mode(command, package); // 决定 copy 或 mount

    ensure_cluster_accepts_runs().await?; // 检查是否有可调度节点

    sync_session_conversation_history(command.user_id, command.session_id).await; // 把旧 run 的回答补回 history
    append_user_message(command.session_id, run_id, command.user_prompt).await?; // 写入本轮用户消息

    let workspace_root = create_workspace(run_id, artifacts).await?; // 创建 /in /out /tmp
    write_run_context(workspace_root, session, package, artifacts, command.user_prompt).await?; // 写上下文文件

    let plan = LaunchPlan { ... }; // 汇总 package / runtime / workspace / provider / k8s 参数
    let outcome = orchestrator.submit_run(plan).await?; // 渲染并提交 ConfigMap / Secret / Job

    let run = build_run_record(run_id, workspace_root, outcome); // 落盘 RunRecord
    store.insert_run(run).await
}
}

6. K3s 资源怎样从业务对象变成 Pod

6.1 ApplicationService(应用服务层)负责业务装配

ApplicationService::create_run 会把会话、文件、挂载、provider 环境变量和 RuntimeProfile 组装成 LaunchPlan

6.2 job_builder 负责渲染资源定义

job_builder::build_bundle(构建运行资源包)会在内存里构造:

  1. 承载 agent package 文件的 ConfigMap
  2. 承载 provider 环境变量的 Secret
  3. 真正执行 run 的 Job

6.3 K3sOrchestrator 负责和集群交互

K3sOrchestrator::submit_run(提交运行)在 apply 模式下会调用:

  • Api<ConfigMap>::create
  • Api<Secret>::create
  • Api<Job>::create

这就是当前 Rust 侧真正把业务运行提交到 K8s 的代码路径。

7. Pod 内执行生命周期

7.1 容器启动后先做什么

job_builder::build_run_script(构建容器启动脚本)会生成启动脚本,脚本大致会:

  1. 创建 /workspace/in/workspace/out/workspace/tmp
  2. 打印 run、session、user、workspace 信息
  3. 如有挂载授权,列出挂载目录文件
  4. 根据 installStrategy 决定是否做运行时安装
  5. 执行 verify 命令
  6. 输出 package 和输入文件信息
  7. 进入具体执行模式

7.2 当前执行模式怎样理解

当前代码里的 ExecutionMode(执行模式)主要有两种:

  • CliVerify
  • OpenAiCompatibleApi

它们表达的是容器内执行入口的行为方式。从架构角度,这仍然属于 agentcore 的执行适配层。

7.3 容器内执行伪代码

容器启动
  -> 读取 /workspace/in/run-context.json
  -> 读取 /opt/agent/package/*
  -> 读取 provider 环境变量
  -> 运行 agentcore
  -> 把主回答写到 /workspace/out/final-answer.md
  -> 把调试信息写到 /workspace/out/*
  -> 进程退出

8. backend 如何知道 run 已经结束

ApplicationService::refresh_run_status(刷新运行状态)会做两类检查。

8.1 第一类:看输出目录

它会检查:

  • runtime-error.txt
  • final-answer.md
  • provider-response.json
  • llm-run-summary.json
  • execution-mode.txt

如果这些文件已经出现,就可以直接推断 run 已完成或失败。

8.2 第二类:看 K8s Job / Pod 状态

如果输出目录还看不出来,backend 会调用 K3sOrchestrator::inspect_run_status(检查运行状态),内部通过:

  • Api<Job>::get_opt
  • Api<Pod>::list

读取 Job 和 Pod,再映射成 RunStatus

8.3 状态刷新伪代码

#![allow(unused)]
fn main() {
async fn refresh_run_status(run: RunRecord) -> RunRecord {
    if let Some(status) = detect_output_status(run.workspace_root.join("out")).await {
        update_run_status(run, status); // 输出已经落盘,直接完成状态刷新
    } else if let Some(observation) = orchestrator.inspect_run_status(&run).await? {
        update_run_status(run, observation); // 读取 Kubernetes Job / Pod 状态
    }

    if run.status == COMPLETED {
        sync_run_assistant_message(&run).await?; // 把 final-answer.md 写回 session history
    }

    persist_run(run).await // 保存新的 run 状态
}
}

9. 同一 session 内如何保持连续性

这是当前生命周期里最关键的一部分。

9.1 文本对话连续性

文本对话连续性由下面两个动作保证:

  1. sync_session_conversation_history(同步会话历史)
    在新 run 开始前刷新同一 session 下旧 run 的状态。
  2. sync_run_assistant_message(同步助手回答)
    在 run 完成后读取 final-answer.md,把它回填进 SessionRecord.history

因此当前同一 session 的下一轮一定可以拿到上一轮的主回答。

9.2 文件结果连续性

run 完成后,当前代码会保留:

  • RunRecord
  • workspace_root
  • workspaces/<run-id>/out/*

因此用户下次回来时,仍能查看历史结果文件。

9.3 上传文件连续性

只要 ArtifactRecorduploads/ 下的文件还在,后续 run 就可以继续引用这些文件。

9.4 agentcore checkpoint 连续性

从运行时模型看,agentcore checkpoint 连续性应采用“同一 session 挂回同一状态目录”的方式。当前代码已经支持:

  • 按 session 保存历史
  • 按 session reopen
  • 按 run 保存结果

当前代码还没有显式支持:

  • session 级状态卷
  • checkpoint_root 元数据
  • 自动把上一轮 checkpoint 挂回下一轮

因此今天已经落地的是“会话历史连续”,即将补齐的是“agentcore 内部状态连续”。

10. 用户离开后再回来,会发生什么

10.1 只是关闭前端页面

如果用户只是关闭前端页面,没有关闭 session,那么后续直接拿原来的 session_id 继续创建 run 即可。

10.2 显式关闭 session

如果用户调用了 close,会话仍保留历史和结果,但会拒绝新的写入。后续调用 reopen 后,同一个 session_id 又可以继续创建新的 run。

10.3 一个月后继续同一个 session

今天这套代码已经能保证下面这些内容在一个月后仍可恢复:

  • 会话历史
  • 上传文件索引
  • 历史 run 记录
  • 历史结果文件

如果还要恢复 agentcore 内部 checkpoint,就需要把 checkpoint 写到持久化挂载目录,并在 backend 里保存该目录索引。这是当前生命周期模型下一步最自然的扩展点。

11. run 结束后,K8s 如何处理资源

当前语义如下:

  • Job / Pod:由 Job TTL 或取消逻辑处理
  • ConfigMap / Secret:取消时会显式删除,正常完成后的统一 GC 还需补齐
  • workspaces/:backend 主机磁盘继续保留
  • state.json:backend 继续保留

K8s 只负责容器对象生命周期,不负责清理对话历史和业务文件索引。

当前实现边界与演进建议

这一章用于把今天代码里已经实现到哪里、还没有实现到哪里,明确说清楚。

1. 当前已经实现的能力

1.1 控制面与状态管理

已经具备:

  • sessionartifactmount grantrun 的完整 API
  • SessionStatusclose / reopen 语义
  • x-user-id 基础所有权隔离
  • 本地 state.json 持久化

1.2 运行时编排

已经具备:

  • RuntimeProfile 装载
  • agent package 装载
  • 每 run 独立 K3s Job
  • package ConfigMap
  • provider Secret
  • Job / Pod 状态观察
  • 集群节点健康与预检 API

1.3 文件管理

已经具备:

  • 上传文件持久化
  • 相对路径保留
  • 每 run 独立工作区
  • 输出文件列举与安全读取
  • mount grant 白名单校验

1.4 多轮对话

已经具备:

  • SessionRecord.history 累积
  • 已完成 run 的主回答回填
  • 同一 session 的 reopen 后继续运行

2. 当前最重要的实现边界

2.1 当前 session 连续性来自持久化状态

当前代码已经连续保存:

  • 对话历史
  • 已上传文件
  • 历史结果文件
  • run 元数据

当前代码还没有复用:

  • 常驻容器进程
  • 进程内缓存
  • shell 状态
  • 工具服务进程

因此当前连续性属于“持久化状态连续”。

2.2 当前还没有显式的 session 级 checkpoint 模型

这是当前最需要被说清楚的边界。

今天的代码里:

  • 每个 run 都有独立 workspace_root
  • 历史结果文件会长期保留
  • 会话历史会长期保留

今天的代码里还没有:

  • checkpoint_root 字段
  • session 级持久化状态卷
  • 自动把上一轮 checkpoint 挂到下一轮
  • resume token / resume handle API

因此,如果 agentcore 本身支持 checkpoint,当前 backend 还需要再补一层“session 级状态目录索引”,才能把 agentcore 的内部状态恢复完整接上。

2.3 当前 runtime family 名称仍是样例标识符

当前源码里的 RuntimeFamily(运行时家族)枚举仍使用样例标识符,例如:

  • Codex
  • ClaudeCode

这不影响运行时架构本身,因为真正决定容器行为的是:

  • image
  • executable
  • installStrategy
  • executionMode

但它说明当前代码在“可扩展 family 标识”上仍有代码级约束。如果后续要把 runtime family 做成完全开放的扩展点,可以把它进一步放宽成字符串或可扩展枚举。

2.4 当前 K8s 资源回收和工作区回收还没有完全打通

当前 run 结束后:

  • Job / Pod 会被 TTL 或取消逻辑处理
  • workspace、artifact、run record、session history 会继续保留
  • 正常完成态 run 的 ConfigMapSecret 还没有统一 GC

因此如果后续 run 数量很多,需要补充后台 GC 或 retention policy,把:

  • workspaces/
  • uploads/
  • ConfigMap
  • Secret

纳入统一清理策略。

2.5 当前工作区基于 hostPath

hostPath 的优点是简单直观、便于单节点 K3s 验证。当前代价也很明确:

  • 强依赖 backend 所在节点的本地磁盘
  • 磁盘压力会直接影响调度
  • 不适合做跨节点迁移
  • 隔离强度弱于更高级的沙箱方案

2.6 当前 backend 采用本地持久化控制面

FileBackedStore 使用本地:

  • state.json
  • uploads/
  • workspaces/

所以现在 backend 更像:

  • 单实例控制面
  • 方便联调和演示的最小实现

多副本水平扩展控制面和外部数据库支撑的生产级 control plane 还需要后续演进。

2.7 当前没有流式输出和事件推送

现在前端 / helper 获取 run 进度主要靠轮询:

  • GET /api/v1/runs/{id}
  • GET /api/v1/runs/{id}/results

当前没有:

  • WebSocket
  • SSE
  • stdout / stderr 实时流
  • token 级别的模型输出流

2.8 当前没有“远端容器直接调用本地用户工具”的桥

这点必须明确写清楚。

当前没有:

  • 本地 Git CLI 代理
  • 本地 shell 代理
  • 本地 IDE / editor RPC
  • 本地文件系统 FUSE 挂载代理

macos-helper 当前只负责文件同步、结果回写和本地 UI 代理。

3. 支持本地工具调用的可行方案

虽然当前没有本地工具桥,但有几种可行方案可以接到现有架构上。

3.1 方案 A:Helper 本地工具 RPC 桥

最自然的方案是让 macos-helper(macOS 本地桥接服务)增加一个本地工具执行层:

  • helper 在本地暴露受控 RPC 接口
  • backend 在创建 run 时下发一组 session 级或 run 级工具描述
  • agentcore 通过受控 API 调用 helper
  • helper 在本机执行 Git、shell、编辑器脚本等工具
  • 执行结果再回传给 agentcore

这个方案需要补齐:

  • 工具白名单
  • 参数校验
  • 审批模型
  • 超时和取消
  • 审计日志
  • 会话级认证

3.2 方案 B:Helper 反向长连接工具隧道

可以让 helper 主动和 backend 保持长连接,由 backend 把工具请求转发给 helper,再由 helper 本地执行。

这个方案的好处是:

  • 后端不需要直接访问用户本机
  • 连接模型更适合 NAT 后的用户机器

需要补齐的能力包括:

  • 双向流式消息协议
  • 工具请求队列
  • 断线重连
  • session / run 级权限绑定

3.3 方案 C:本地只负责文件,同步后由远端容器执行

这是当前代码最接近的模式:

  • helper 只负责把本地目录同步成 artifact
  • 远端容器只处理文件,不直接调用本地工具

它实现简单,安全边界清楚,但能力上限较低。

3.4 方案 D:受控宿主机工具代理

如果未来不仅要访问本地工具,还要访问 backend 所在 Linux 主机上的受控工具,也可以增加宿主机工具代理层。

这个方案需要非常严格的:

  • 白名单
  • 身份隔离
  • 审计
  • 资源限制

因为它的权限风险明显高于文件同步。

4. 当前架构为什么仍然是合理的

尽管上面有不少边界,但以当前代码目标来看,这套方案仍然成立,因为它先解决了最核心的几件事:

  1. 把 Agent 运行从 backend 进程里剥离到了独立容器
  2. agent package、运行时模板、provider、workspace 拆成了清晰的组合关系
  3. 把 session 多轮上下文和 run 物理隔离分开建模
  4. 让前端、helper、backend、K3s 之间有了可验证的 API 契约

5. 如果继续演进,优先级建议是什么

5.1 第一优先级:补齐 session 级状态目录

优先补齐:

  • checkpoint_root
  • session 级持久化状态卷
  • resume / restore API
  • 输出文件到下一轮输入的受控提升机制

这是把“会话历史连续”升级成“完整运行时状态连续”的关键一步。

5.2 第二优先级:把“声明型策略”变成“强执行策略”

优先补齐:

  • permissionProfile 到真实能力矩阵
  • networkProfile 到真实 NetworkPolicy
  • builtInTools 到真实工具调度层

5.3 第三优先级:补齐本地工具桥

建议优先落地 Helper 侧 RPC 桥,因为它和当前“本地 Helper + 云端 backend”结构最匹配。

5.4 第四优先级:把工作区从 hostPath 升级出去

重点是:

  • 降低宿主机磁盘压力对调度的直接影响
  • 支持更强的多节点与弹性调度
  • 把 workspace 生命周期和 node 生命周期更好地解耦

5.5 第五优先级:把 backend 升级成可水平扩展控制面

也就是逐步把:

  • state.json
  • 本地 uploads
  • 本地 workspaces 元数据

迁移到更适合多副本的外部存储体系。

6. 当前技术方案的最终判断

如果基于当前源码评价,这套 backend 已经具备“Agent 商店运行时最小可用控制面”的完整骨架:

  • 会话有了
  • 文件有了
  • 挂载有了
  • run 有了
  • K3s 编排有了
  • provider 注入有了
  • 多轮历史有了
  • 节点预检和状态观察也有了

当前离“生产级多租户高隔离 Agent Runtime 平台”的主要差距集中在:

  • session 级 checkpoint 连续性
  • 本地工具桥
  • 强权限
  • 强网络策略
  • 工作区存储外部化
  • 控制面外部化持久存储