Files
Agent-System-Backend/internal/execenv/wheel.go
T
weichenandCursor b85a1e1ca3 Add immutable layered Python environments for the sandbox.
Why: The sandbox hides the host home directory and has no network, so pip install --user cannot supply python-docx, and the service PATH resolved python3 to a linuxbrew path that does not exist inside the sandbox.
Evidence: SELF go test of execenv, tools, store, app, server, runmgr, config, and cmd/agent-system on this worktree. CI has not run.
Impact: Sessions inherit a read-only base layer. Extra packages are requested through python_env, auto-approved only from the allowlist, and mounted per conversation. Schema 35 adds the request and binding tables.
Refs: .task/feature-layered-python-env.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 13:52:48 +00:00

113 lines
2.8 KiB
Go

package execenv
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// UnpackWheel 把 wheel 解进 site-packages。wheel 是 zip;这里不执行任何入口脚本。
func UnpackWheel(wheelPath, sitePackages string) error {
zr, err := zip.OpenReader(wheelPath)
if err != nil {
return fmt.Errorf("execenv: open wheel: %w", err)
}
defer zr.Close()
if err := os.MkdirAll(sitePackages, 0o755); err != nil {
return err
}
root := filepath.Clean(sitePackages)
for _, f := range zr.File {
name := filepath.Clean(f.Name)
if name == "." || strings.HasPrefix(name, "..") || filepath.IsAbs(name) {
return fmt.Errorf("execenv: wheel path escapes: %s", f.Name)
}
// 安装器脚本不落地,避免把「解包」变成「执行」。
if strings.Contains(name, ".data/scripts/") {
continue
}
dest := filepath.Join(root, name)
if !strings.HasPrefix(dest, root+string(os.PathSeparator)) && dest != root {
return fmt.Errorf("execenv: wheel path escapes: %s", f.Name)
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
rc.Close()
return err
}
_, cerr := io.Copy(out, rc)
out.Close()
rc.Close()
if cerr != nil {
return cerr
}
}
return nil
}
// BuildLayer 把已经在 wheelhouse 里的 wheel 解进新层。目录已存在则直接复用。
func (r *Root) BuildLayer(l Lock) (string, error) {
id, err := r.WriteLock(l)
if err != nil {
return "", err
}
dest := r.LayerDir(id)
if st, err := os.Stat(dest); err == nil && st.IsDir() {
return id, nil
}
for _, pkg := range l.Packages {
wheel := filepath.Join(r.Wheelhouse(), pkg.SHA256+".whl")
if _, err := os.Stat(wheel); err != nil {
alt := filepath.Join(r.Wheelhouse(), pkg.Filename)
if _, err2 := os.Stat(alt); err2 != nil {
return "", fmt.Errorf("execenv: wheel missing for %s", pkg.Name)
}
wheel = alt
}
sum, err := fileSHA256(wheel)
if err != nil {
return "", err
}
if pkg.SHA256 != "" && !strings.EqualFold(sum, pkg.SHA256) {
return "", fmt.Errorf("execenv: hash mismatch for %s: got %s want %s", pkg.Name, sum, pkg.SHA256)
}
if err := UnpackWheel(wheel, dest); err != nil {
_ = os.RemoveAll(filepath.Dir(dest))
return "", err
}
}
if err := os.MkdirAll(dest, 0o755); err != nil {
return "", err
}
return id, nil
}
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return h.Sum(), nil
}