package execenv // executor.go - Flyto 子进程启动的统一抽象入口 (M1 "门框"). // // 本文件只定义 interface / Spec / Class / Process 四个核心类型, 不含实现. // 实现分两份: // - DefaultExecutor (本地 CLI, 本包 default_executor.go, 零开销包装 os/exec) // - platform/common/internal/sandbox.Backend (云端 SaaS, E2B Firecracker 后端) // // 本文件是 platform/common/internal/sandbox/DESIGN.md section 7 / 7.1 的代码体现. // 设计契约变更必须先改 DESIGN.md 再改本文件, 不要反向. // // # 禁止方向 (DESIGN.md section 7 写死的红线) // // 1. 不要把 *exec.Cmd 暴露在 Executor/Process 公开 API 里. 会锁死 POSIX // 进程模型, 未来换 WASM runtime 或远程 RPC 后端无法兼容. // // 2. 不要在 Spec 里加只对某一个 Class 有意义的字段 (如 NetworkPolicy). // 跨 Class 通用字段才能进 Spec, Class-specific 策略属 platform 层 // 的后端配置, core 只传 Class 值. // // 3. 不要在 core 里做 Class -> 隔离策略的映射. 映射逻辑属 platform // sandbox 包, core 只负责透传 Class 标签. // // 违反这三条的任何实现会让 platform 层的 sandbox DI 无法干净接入, // 整个 M1 白做. 本注释是最后一道防线, 不要删. import ( "context" "io" "os" ) // 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 Executor interface { Command(ctx context.Context, spec Spec) Process } // Spec 是沙盒后端需要知道的最小信息集. 字段按信任模型重要性排序, // Class 最关键 - 它告诉后端 "这是哪个入口发来的", 后端据此决定隔离强度. // // 只装跨 Class 通用信息. 不要加只对某一个 Class 有意义的字段 // (如 NetworkPolicy / FSReadOnly / MaxCPUQuota), 那些应放在 platform // 层的后端配置里, 由后端根据 Class 查策略表决定. 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 } // 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. type Class int 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 ) // 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 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 }