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>
113 lines
2.8 KiB
Go
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
|
|
}
|