Documentation
¶
Overview ¶
Package execenv abstracts process execution so tool handlers (Bash, SubAgent fork, plugin sandboxing) can run against a local exec.Cmd in CLI / daemon mode or against a remote sandbox (E2B Firecracker, etc.) in SaaS mode. The Executor / Process pair is the sole injection point for "how do we actually run a child process" — everything above it calls these interfaces, nothing below assumes local fork.
API Consumption Shapes ¶
Both Executor and Process are form 3 (synchronous callback) under the three Flyto API shapes (see `docs/api-reference.md` "API 消费形态 / API Consumption Patterns"):
- Executor.Start: engine / tools call synchronously to launch a child process; the backend implementation (local / sandbox) returns a Process handle or error.
- Process.Wait / Kill / Stdin / Stdout / Stderr: synchronous method calls on the returned handle, same shape.
Package execenv 抽象进程执行: 工具处理器 (Bash / SubAgent fork / plugin 沙盒) 在 CLI / daemon 模式下跑本地 exec.Cmd, 在 SaaS 模式下跑远端沙盒 (E2B Firecracker 等). Executor / Process 对是 "我们怎么真执行子进程" 的 唯一注入点 —— 上层都调这对接口, 下层不假设本地 fork.
API 消费形态 ¶
Executor 和 Process 都是形态三 (同步回调, 见 `docs/api-reference.md` "API 消费形态 / API Consumption Patterns"):
- Executor.Start: 引擎 / 工具同步调启动子进程; 后端实现 (本地 / 沙盒) 返 Process 句柄或 error.
- Process.Wait / Kill / Stdin / Stdout / Stderr: 对返回句柄的同步方法调用, 同形态.
Package execenv 提供 Flyto 引擎派生子进程 (plugin shell tool / MCP stdio server / 未来其他 subprocess transport) 时的环境变量策略.
设计目标
- 零信任默认: Flyto 主进程的 ANTHROPIC_API_KEY / OPENAI_API_KEY / AWS_SECRET_ACCESS_KEY 等敏感 env **永远不会** 隐式泄漏到子进程.
- 显式透传: 子进程需要的 env 必须在配置里显式声明 (plugin.json 的 tools[].env / mcp_servers[].env, 或 settings.json 的 mcpServers[].env).
- ${VAR} 展开: 配置值里的 ${NAME} 会从宿主 env 查 NAME, 查不到返回 error (启动失败比静默降级更安全, 避免秘密变成空字符串悄悄吃掉).
为什么是 internal
此包是 plugin 包和 internal/mcp 包共享的底层 env 策略实现. 两个上层包都 import 它, 它不 import 任何 Flyto 自有包, 位于 import 图最底层. 放在 internal/ 的作用是**明确不作为 SDK 公开接口**: 消费者 (如飞驼云仓平台) 不应直接调用 execenv, 而应通过 plugin 包或 MCP 配置的上层 API 间接受益.
关于 lifecycle 隔离 (见 engine.go 的 parsePluginMCPServerKey)
本包的 env 白名单对 plugin-owned 和 user-configured 的 MCP server **一视 同仁**, 不看 server key 前缀. lifecycle 前缀区分是**正交机制**: 它解决 "禁用 plugin 时不能误伤用户配置的 server" 的问题, 和 env 策略是两回事. 反向考虑过给 user-configured 放宽 env (继承 os.Environ), 但结论是双轨制 在 Flyto 目标场景 (B2B SaaS, 飞驼云仓等) 下宽松那一轨永远是死代码, 反而 引入跨场景行为不一致. 详见设计讨论记录.
Index ¶
- func ExpandEnvMap(in map[string]string) (map[string]string, error)
- func FullInheritMap(extra map[string]string) map[string]string
- func MinimalEnv(extra map[string]string) []string
- func MinimalEnvMap(extra map[string]string) map[string]string
- type Class
- type DefaultExecutor
- type Executor
- type Process
- type Spec
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExpandEnvMap ¶
ExpandEnvMap 对 map 的 value 做 ${VAR} / $VAR 展开, 从宿主 os env 查值.
语法: Go 标准 os.Expand (与 shell / Docker Compose / Kubernetes envFrom 大致一致). ${NAME} 和 $NAME 都支持. 字面 $ 用 $$ 转义. 复杂语法 (默认值 ${VAR:-x}, 子串 ${VAR:0:3}) 不支持, YAGNI.
失败模式: 引用了**未设置**的环境变量 → 立刻返回 error, 不展开成空字符串. 这是显式选择: 如果用户配置 "API_KEY": "${BRAVE_KEY}" 而 BRAVE_KEY 未 set, 静默降级为空字符串会让子进程启动但 API 调用 401, debug 困难. 硬错误强迫 用户在启动期就修配置.
注意: 显式设置为空字符串 (export FOO=) 与未设置不同. 前者合法, 后者 error. 这依赖 os.LookupEnv 的区分能力, os.Getenv 不能区分.
in 为 nil 或空时返回 nil, nil.
func FullInheritMap ¶
FullInheritMap 构造子进程的全继承环境变量 map, 用于 execenv.Spec.Env 字段.
语义: os.Environ() 全量继承 + extra 覆盖同名 key. 返回 map[string]string, 让类 B (全继承) 入口可以直接赋给 Spec.Env 字段而不触发 nil-vs-empty 陷阱.
使用场景: ClassUserHook / ClassEvolve / ClassBash / ClassMemoryGit 等 "用户对自己机器负责" 的入口, 威胁模型和 Git hooks / systemd Exec= / npm pre-scripts 一致. 和 MinimalEnvMap 对称, 区别只在 base 是全宿主 env 还是 白名单 4 个 key.
返回值保证 non-nil. 即使宿主 env 为空且 extra 为 nil, 也返回空的 non-nil map, 对齐 MinimalEnvMap 的 nil-safety 约定.
合并规则 (last-wins, 和 MinimalEnvMap 一致):
- os.Environ() 全部条目进 base
- extra 追加, 同名 key **覆盖** base
陷阱警告: os.Environ() 可能包含重复 key (POSIX 允许). Go 的 os.Environ 不去重, 按插入顺序返回, last-wins. 这里转 map 时自然取 last, 和 *exec.Cmd.Env = os.Environ() 的语义一致 (os/exec 也会把 map 构造的 env 重新 last-wins 处理).
强制约定: 类 B (全继承) 入口的调用方**永远**走此 helper 构造 Spec.Env, 永远不自己写 os.Environ() 切片拼装, 让 Spec.Env 字段的 map 类型契约统一.
func MinimalEnv ¶
MinimalEnv 构造子进程的最小环境变量集合.
返回值是 "KEY=VALUE" 的切片, 可直接赋给 exec.Cmd.Env.
合并规则 (POSIX last-wins, Go os/exec 文档保证此行为):
- 白名单 OS env (存在才透传)
- extra 声明的 env 追加, 同名 key **覆盖**白名单
所以调用方可以:
- 覆盖 HOME (比如沙盒到 plugin 目录)
- 清除 PATH (传空字符串)
- 注入自己的 API key
extra 为 nil 或空时返回纯白名单.
内部实现基于 buildMinimalEnv helper, 和 MinimalEnvMap 共享合并规则, 保证两个函数永远同步演进 (白名单新增项 / extra 合并策略变更同时生效).
func MinimalEnvMap ¶
MinimalEnvMap 构造子进程的最小环境变量 map, 用于 execenv.Spec.Env 字段.
语义等同 MinimalEnv 但返回 map[string]string, 让 Executor caller 可以 直接赋给 Spec.Env 字段, 避免手动 split "KEY=VALUE" 字符串.
返回值保证 non-nil (即使 extra == nil 或白名单一个都不在宿主 env, 也 返回空的 non-nil map). 这让调用方可以安全赋给 Spec.Env 字段, 不会触发 DefaultExecutor.mapToEnvSlice 的 nil-vs-empty 陷阱 (见 executor.go 的 Spec.Env 字段注释警告).
强制约定: 类 A (白名单) 入口的调用方**永远**走此 helper 构造 Spec.Env, 永远不自己写 map[string]string{} 或 nil, 让陷阱在产品代码路径上不可达.
Types ¶
type Class ¶
type Class int
Class 是租户隔离粒度和信任级别的唯一信号源. 沙盒后端据此决定:
- 是否复用 microVM (同 session 内 bash 可复用, plugin_mcp 每个单独起)
- 是否开网络 (user_hook 全开, plugin_mcp 默认关)
- 是否挂载 ~/.flyto (memory_git 需要, plugin_tool 不需要)
- 是否允许宿主 FS bind mount (Bash 的 WorkDir 需要, plugin 默认不)
Class -> 隔离策略的映射逻辑属 platform sandbox 包, core 只传 Class 值. 详见 platform/common/internal/sandbox/DESIGN.md section 7.1.
const ( // ClassPluginMCP 是第三方 plugin 的 MCP stdio subprocess. // 陌生代码长驻, 最强隔离, no-net, no-fs. L511 威胁模型. ClassPluginMCP Class = iota // ClassPluginTool 是 plugin 声明的 shell tool 和 memory external scorer. // 陌生代码, 短生命期可复用 sandbox. 同 PluginMCP 的隔离策略. ClassPluginTool // ClassPluginHook 是 plugin-owned hook (HookDef.PluginDir != ""). // 同 PluginMCP 策略. L513 威胁模型. ClassPluginHook // ClassUserHook 是用户自配 hook (settings.json) 和静态声明的 exec tool. // 云端场景不存在 (云端没有 settings.json), 本地信任用户, 全继承 env. // 对齐 Git hooks / systemd Exec= / npm pre-scripts 默认语义. ClassUserHook // ClassEvolve 是 LLM 生成 + 人类审批的 evolve 脚本/命令. // 信任度中, 按 session 隔离. 本地全继承 env (L514 决策), // 云端 microVM 里根本没有敏感 env 可继承, 自然收紧. ClassEvolve // ClassBash 是 Agent 主执行通道 (bash.go / bash_background.go). // 信任度中, 按 session 隔离 + 网络白名单. 本地全继承 env (L513 决策). // 秘密管理走 SecretStore, 不做 Bash 内部 env 黑名单. ClassBash // ClassMemoryGit 是引擎自身 memory git 同步. // 特殊 system 沙盒或直接在 platform pod 内 (引擎自用, 不是租户代码). // 需要 SSH_AUTH_SOCK / HOME / GIT_AUTHOR_* 才能正常 git push. ClassMemoryGit // ClassWorkspaceTool 是引擎对租户 workspace 的系统工具调用 // (ripgrep / git ls-files / git worktree). 和同租户 ClassBash 共享 // microVM 实例, 只 bind /workspace, 无网络. // // 2026-04-15 方案 C 新增, 覆盖 M1 初版漏掉的 4 个入口 (grep_engine / // glob_engine / worktree). 详见 platform/common/internal/sandbox/DESIGN.md // section 7.1. // // 语义边界: "引擎自己调系统工具对租户 workspace 操作", 不是 // "租户代码跑在 workspace". 前者属 ClassWorkspaceTool, 后者属 // ClassBash / ClassEvolve. 两者在 microVM 复用上一致, 但审计归属 // 和未来策略演进路径不同, 不要合并. ClassWorkspaceTool )
type DefaultExecutor ¶
type DefaultExecutor struct{}
DefaultExecutor 是本地 CLI 模式的默认 Executor 实现.
Class 和 TenantID 在本地模式被忽略 - local CLI 信任用户, 不按 Class 差异化隔离 (见 project_sandbox_local_vs_cloud memory). 行业对齐: Claude Code / Cursor / Aider / VS Code tasks 全都如此.
func (DefaultExecutor) Command ¶
func (DefaultExecutor) Command(ctx context.Context, spec Spec) Process
Command 零开销包装 exec.CommandContext + Spec 字段映射.
不返回 error: *exec.Cmd 的错误 (Path 不可达 / fork 失败 / 资源不足) 全都在 Start() 时才报告, 和 Go 标库语义一致.
Spec 字段映射策略:
- Env/Dir/Stdin: 原样映射
- Stdout/Stderr: Writer 模式直接赋给 cmd.Stdout/Stderr (nil 保持默认)
- IsolateProcessGroup: 映射为 Setpgid + Pgid 0 (和 bash/bash_background 现有写法一致). Linux/Mac only; Windows 不支持此字段会编译失败, 对齐 Flyto core 当前的 POSIX-only 约束.
- Class/TenantID: 本地模式忽略
type Executor ¶
Executor 是所有 Flyto 子进程的统一启动抽象. 实现方:
- DefaultExecutor (本地 CLI, 本包 default_executor.go)
- platform/common/internal/sandbox.Backend (云端 SaaS, E2B 后端)
Command 只做结构体字段映射, 零系统调用. 真正的进程创建发生在 Process.Start() 调用时. 这一分层对齐 *exec.Cmd 语义, 让 backend 可以在 Start() 时才起 microVM / 分配资源 / 校验 Path 可达.
Command 不返回 error: 错误延迟到 Start() 时报告, 让 Command 成为 纯字段映射函数 (可缓存 / 可无副作用重试 / 可 mock).
type Process ¶
type Process interface {
// Start 启动子进程. 非阻塞, 和 *exec.Cmd.Start() 语义一致.
// 错误场景: Path 不可达 / 权限不足 / 资源耗尽.
Start() error
// Wait 等待子进程退出, 返回 exit error (*exec.ExitError 或其他).
// 必须在 Start() 成功后调用. 不能重复 Wait 同一进程.
Wait() error
// Run 便捷方法: Start() 然后 Wait() 一步.
// 等价于 *exec.Cmd.Run(). 如果 Start 或 Wait 失败, 返回对应 error.
//
// 大量 caller (hooks / evolve / memory git / grep_engine / plugin_tool)
// 用 cmd.Run() 一步跑完, 不想手写 Start+Wait 样板. 加入便捷方法让
// 迁移成本最小化. backend 实现 = trivial 的 Start+Wait 包装.
Run() error
// StdinPipe 返回子进程标准输入的写入端. 必须在 Start() 前调用.
// 返回的 WriteCloser 在 Wait() 后自动关闭.
//
// 和 Spec.Stdin 字段互斥: 设了 Stdin 字段不能再调 StdinPipe,
// 对齐 *exec.Cmd 语义. 长驻双向通信场景 (stdio transport / memory
// external scorer) 用 StdinPipe.
StdinPipe() (io.WriteCloser, error)
// StdoutPipe 返回子进程标准输出的读取端. 必须在 Start() 前调用.
// 返回的 ReadCloser 在 Wait() 后自动关闭.
// 和 Spec.Stdout 字段互斥.
StdoutPipe() (io.ReadCloser, error)
// StderrPipe 返回子进程标准错误的读取端. 必须在 Start() 前调用.
// 返回的 ReadCloser 在 Wait() 后自动关闭.
// 和 Spec.Stderr 字段互斥.
StderrPipe() (io.ReadCloser, error)
// Signal 向子进程发送任意信号 (SIGINT / SIGTERM / SIGHUP 等).
// 等价于 *exec.Cmd.Process.Signal(sig). 如果进程未 Start 或已退出,
// 返回 error. 用于 graceful shutdown 路径 (先 SIGINT 后 SIGKILL).
//
// 云端 backend 映射为信号转发到 microVM 内部的进程. 不支持的信号
// (如某些实时信号) 由 backend 决定是映射还是 error.
Signal(sig os.Signal) error
// Kill 强制终止子进程 (SIGKILL 等价). 幂等: 未 Start 或已 Wait
// 后调用返回 nil, 不 panic.
//
// Kill 是 Signal(syscall.SIGKILL) 的快捷方式, 单独保留是因为它是
// 最常见的终止路径, 且幂等保证让 defer p.Kill() 可以无条件使用.
Kill() error
// SignalGroup 向子进程所在的 process group 发送信号.
//
// 和 Signal 的区别: Signal 只作用单个子进程 (叶子), SignalGroup
// 作用整个 process group (含子 shell 派生的孙进程). bash 的
// graceful kill 路径和 plugin_tool 的 force kill 路径依赖此语义
// 来彻底清理进程树, 防止孤儿进程继续持有 /workspace fd 或网络端口.
//
// # 前置条件
//
// 必须在创建时设 Spec.IsolateProcessGroup=true, 让子进程成为独立
// pgid 的 leader. 否则子进程继承父进程的 pgid, SignalGroup 会
// 精确退化为 Signal 单进程语义 — 不会对父 pgid 发信号, 因为那
// 会把宿主 (engine 本身) 一起杀掉. 退化路径让此方法在任何场景下
// 调用都安全, 但 caller 应遵守前置条件获得真正的 group 语义.
//
// # Backend 语义
//
// - 本地 DefaultExecutor: syscall.Kill(-pgid, sig) on Unix.
// Windows 退化为 Kill (Job Object 方案未来再做, 当前 core
// 已是 POSIX-only 约束, 详见 default_executor.go 注释).
// - 云端 backend: 信号转发给 microVM 内整个 pgid, 或直接信号
// microVM 本身 (等效).
//
// # 为什么不在 Signal 里加 group 参数
//
// Signal 语义对齐 *exec.Cmd.Process.Signal (单进程), 改签名会
// 破坏所有现有 caller. SignalGroup 作为独立方法让现有 Signal
// caller (SIGHUP / SIGINT 的 graceful shutdown 单进程路径) 零破坏.
//
// 2026-04-15 commit 7a 新增, 为 bash/plugin_tool 的 pgid-level
// kill 路径提供抽象. 是 M1 唯一**必须**扩容的 interface 方法 —
// duck-type 解决不了 group 语义. 详见 platform/sandbox/DESIGN.md
// section 7 和 next-session-task.md 的 pre-audit.
SignalGroup(sig os.Signal) error
// Output 便捷方法: Run + 读取 stdout 全部字节. 等价于 *exec.Cmd.Output().
// 不能和 StdoutPipe / Spec.Stdout 混用.
//
// 为什么加这个便捷方法 (超出 DESIGN section 7 原 5 方法):
// syslib/git + worktree + context 等代码大量用 cmd.Output() 一步拿
// stdout. 如果 Process 只有 pipe + start + wait, 每个 caller 要多写
// 5-10 行 pipe+read+wait 样板, 迁移成本翻倍. backend 实现成本 =
// 一次性写 start+read+wait 包装.
Output() ([]byte, error)
// CombinedOutput 便捷方法: Run + 读取 stdout+stderr 合并字节.
// 等价于 *exec.Cmd.CombinedOutput(). 不能和任何 Pipe / Spec Writer 混用.
CombinedOutput() ([]byte, error)
// ID 返回后端无关的进程标识符, 用于日志 / 任务追踪 / UI 展示.
//
// 本地 DefaultExecutor 返回 strconv.Itoa(cmd.Process.Pid), 云端
// backend 返回 microVM_id 或 task_id. caller 只能把它当 opaque 字符串
// 用, 不应解析回 pid 做进程操作 — 那是非 portable 的 POSIX 假设,
// 违反 "不锁死 POSIX 进程模型" 红线.
//
// 如果进程未 Start 或 ID 不可用, 返回空字符串.
ID() string
}
Process 是启动后的子进程句柄. 方法语义严格对齐 *exec.Cmd, 让 DefaultExecutor 可以直接包装而无需逻辑翻译, 同时给云端 backend 一个清晰的实现契约.
刻意不暴露 *os.Process / ProcessState 等底层结构, 让云端 backend 可以用 microVM / 远程 RPC / WASM runtime 等非 POSIX 实现.
Method 分组 ¶
Lifecycle: Start, Wait, Run I/O pipes: StdinPipe, StdoutPipe, StderrPipe (和 Spec.Stdin/Stdout/Stderr 二选一) Signals: Signal, Kill (Signal 发任意信号, Kill 是 SIGKILL 快捷) Convenience: Output, CombinedOutput (Start + 读 + Wait 一步) Meta: ID
caller-driven audit (2026-04-15) ¶
原 commit 1 的 7 方法是凭空设计, caller-driven audit 暴露缺口后扩到 11 方法. 新增 StdinPipe / Signal / Run / ID. 详见 DESIGN.md section 7.
type Spec ¶
type Spec struct {
// Class 是租户隔离粒度和信任级别的唯一信号源. 必填.
Class Class
// Path 是可执行文件路径 (宿主视角). 云端 backend 负责翻译成
// 容器内路径 (例如 /usr/bin/git 在 microVM 里也是 /usr/bin/git).
Path string
// Args 是 Path 之后的参数列表, 不包含 Path 本身.
// 对齐 exec.Command(name, arg ...string) 的第二参数语义.
Args []string
// Env 是经 execenv helper 构造后的最终环境变量 map.
// 已按信任类归类 (白名单类 A / 全继承类 B), backend 不再做过滤.
//
// 陷阱警告: nil map 会让 DefaultExecutor 走 *exec.Cmd 默认行为
// (继承 os.Environ 全部变量), 不是 "空 env". 类 A 白名单调用方
// 必须传非 nil 的 map (通过 MinimalEnvMap helper 构造), 否则
// 白名单失守, 秘密泄漏到陌生代码子进程.
//
// 强制约定: 调用方永远不自己写 map[string]string{} 或 nil,
// 永远走 execenv 包的 helper 函数 (MinimalEnvMap / FullInheritMap
// 等, 将在迁移阶段陆续新增). 这让陷阱在产品代码路径上不可达.
Env map[string]string
// Stdin 是子进程标准输入 (Reader 模式). 可为 nil 表示无 stdin.
//
// 和 StdinPipe() 方法二选一: 设了 Stdin 字段不能再调 StdinPipe,
// 对齐 *exec.Cmd 语义. 大部分 caller 用 Stdin 字段 (简单场景),
// 只有长驻双向通信 (stdio transport / memory scorer) 用 StdinPipe.
Stdin io.Reader
// Stdout 是子进程标准输出的写入目标 (Writer 模式). 可为 nil 表示
// 丢弃 (走 /dev/null) 或走 StdoutPipe.
//
// 和 StdoutPipe() 方法二选一: 设了 Stdout 字段不能再调 StdoutPipe,
// 对齐 *exec.Cmd 语义. 2026-04-15 caller-driven audit 发现 8+ caller
// 用 Writer 模式 (cmd.Stdout = &bytes.Buffer{}), 只有 bash / bash_background
// 用 Pipe 模式. 不支持 Writer 模式会让迁移成本暴涨, 所以加入 Spec.
Stdout io.Writer
// Stderr 是子进程标准错误的写入目标 (Writer 模式). 可为 nil.
// 和 StderrPipe() 方法二选一. 语义和 Stdout 对称.
Stderr io.Writer
// WorkDir 是子进程工作目录 (LLM 感知的 cwd).
// 云端 backend 翻译为 /workspace 的 bind mount 挂载点.
WorkDir string
// IsolateProcessGroup 请求后端把子进程隔离到独立进程组.
//
// 本地模式: DefaultExecutor 映射为 syscall.SysProcAttr{Setpgid: true, Pgid: 0},
// 让 Kill/Signal 能作用于整个进程树 (防止子 shell 派生的孙进程成为孤儿).
// 云端模式: E2B microVM 天然是隔离边界, 后端忽略此字段.
//
// 意图级字段, 刻意不暴露 POSIX 的 SysProcAttr 结构 — 后者是跨平台
// 不兼容的底层 API, 违反 "Spec 只装跨 Class 通用信息" 红线.
// ClassBash 的 caller (bash / bash_background) 必须显式设为 true,
// 其他 Class 按需.
IsolateProcessGroup bool
// TenantID 是多租户隔离的身份标签. platform 层填充.
// DefaultExecutor 忽略此字段 (本地 CLI 无多租户概念).
//
// core 层永远不自己填 TenantID, 这是 platform 层 tenant 包
// 从 HTTP request 上下文解析后在 engine.New 之前注入的.
TenantID string
}
Spec 是沙盒后端需要知道的最小信息集. 字段按信任模型重要性排序, Class 最关键 - 它告诉后端 "这是哪个入口发来的", 后端据此决定隔离强度.
只装跨 Class 通用信息. 不要加只对某一个 Class 有意义的字段 (如 NetworkPolicy / FSReadOnly / MaxCPUQuota), 那些应放在 platform 层的后端配置里, 由后端根据 Class 查策略表决定.