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>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
// Package execenv 是系统托管的 Python 环境:一个解释器、一份不可变基础层、
|
|
// 按内容哈希缓存的扩展层。工作区不各自建 venv,也不往一个可变全局环境里装包。
|
|
package execenv
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Root 是环境根目录。多个服务实例可以共用同一份。
|
|
type Root struct {
|
|
Path string
|
|
}
|
|
|
|
// Open 创建环境根的目录骨架。已存在的层与锁不会被改写。
|
|
func Open(path string) (*Root, error) {
|
|
if strings.TrimSpace(path) == "" {
|
|
return nil, errors.New("execenv: empty root")
|
|
}
|
|
r := &Root{Path: path}
|
|
for _, d := range []string{
|
|
r.PythonDir(), r.LayersDir(), r.Wheelhouse(), r.LocksDir(), r.BinDir(),
|
|
} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := r.ensureCatalog(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writePipShim(r.BinDir()); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Root) PythonDir() string { return filepath.Join(r.Path, "python") }
|
|
func (r *Root) LayersDir() string { return filepath.Join(r.Path, "layers") }
|
|
func (r *Root) Wheelhouse() string { return filepath.Join(r.Path, "wheelhouse") }
|
|
func (r *Root) LocksDir() string { return filepath.Join(r.Path, "locks") }
|
|
func (r *Root) BinDir() string { return filepath.Join(r.Path, "bin") }
|
|
func (r *Root) CatalogPath() string { return filepath.Join(r.Path, "catalog.json") }
|
|
|
|
func (r *Root) ensureCatalog() error {
|
|
if _, err := os.Stat(r.CatalogPath()); err == nil {
|
|
return nil
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
raw, err := json.MarshalIndent(DefaultCatalog(), "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(r.CatalogPath(), append(raw, '\n'), 0o644)
|
|
}
|
|
|
|
// LayerDir 返回某一层的 site-packages 目录(不一定已经存在)。
|
|
func (r *Root) LayerDir(id string) string {
|
|
return filepath.Join(r.LayersDir(), id, "site-packages")
|
|
}
|
|
|
|
// CurrentBase 读取 current 符号链接指向的基础层 ID。未建立时返回空串。
|
|
func (r *Root) CurrentBase() (string, error) {
|
|
dest, err := os.Readlink(filepath.Join(r.Path, "current"))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
id := filepath.Base(dest)
|
|
if id == "" || id == "." {
|
|
return "", fmt.Errorf("execenv: current link %q is empty", dest)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// SwitchCurrent 把 current 指到已存在的层。旧链接被替换,层目录本身不动。
|
|
func (r *Root) SwitchCurrent(id string) error {
|
|
if _, err := os.Stat(r.LayerDir(id)); err != nil {
|
|
return fmt.Errorf("execenv: layer %s: %w", id, err)
|
|
}
|
|
link := filepath.Join(r.Path, "current")
|
|
tmp := link + ".tmp"
|
|
_ = os.Remove(tmp)
|
|
if err := os.Symlink(filepath.Join("layers", id), tmp); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, link)
|
|
}
|
|
|
|
// ReadLock 读取某一层的锁。
|
|
func (r *Root) ReadLock(id string) (Lock, error) {
|
|
raw, err := os.ReadFile(filepath.Join(r.LocksDir(), id+".json"))
|
|
if err != nil {
|
|
return Lock{}, err
|
|
}
|
|
var l Lock
|
|
if err := json.Unmarshal(raw, &l); err != nil {
|
|
return Lock{}, err
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// WriteLock 写入锁文件。内容按哈希寻址,已存在且字节相同则跳过。
|
|
func (r *Root) WriteLock(l Lock) (string, error) {
|
|
id := l.ID()
|
|
path := filepath.Join(r.LocksDir(), id+".json")
|
|
raw, err := json.MarshalIndent(l, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
raw = append(raw, '\n')
|
|
if prev, err := os.ReadFile(path); err == nil {
|
|
if string(prev) != string(raw) {
|
|
return "", fmt.Errorf("execenv: lock %s already exists with different bytes", id)
|
|
}
|
|
return id, nil
|
|
}
|
|
return id, os.WriteFile(path, raw, 0o644)
|
|
}
|
|
|
|
// HashBytes 是层键与 wheel 去重共用的 sha256。
|
|
func HashBytes(b []byte) string {
|
|
sum := sha256.Sum256(b)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|