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>
98 lines
2.5 KiB
Go
98 lines
2.5 KiB
Go
package execenv
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Doctor 用给定解释器导入模块。sandbox 为真且系统有 bwrap 时,按与执行后端相同的隔离方式跑。
|
|
func Doctor(ctx context.Context, python string, pyPath []string, modules []string, sandbox bool, mounts []string) error {
|
|
if python == "" {
|
|
return fmt.Errorf("execenv: doctor: python is empty")
|
|
}
|
|
if len(modules) == 0 {
|
|
modules = []string{"docx", "openpyxl", "pptx"}
|
|
}
|
|
script := "import " + strings.Join(modules, ", ")
|
|
if !sandbox {
|
|
cmd := exec.CommandContext(ctx, python, "-c", script)
|
|
cmd.Env = doctorEnv(pyPath)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("execenv: doctor: %v: %s", err, trim(out))
|
|
}
|
|
return nil
|
|
}
|
|
bwrap, err := exec.LookPath("bwrap")
|
|
if err != nil {
|
|
return fmt.Errorf("execenv: doctor: sandbox requested but bwrap is missing: %w", err)
|
|
}
|
|
argv := []string{"--unshare-all", "--die-with-parent", "--ro-bind", "/", "/", "--tmpfs", "/home", "--tmpfs", "/tmp", "--proc", "/proc", "--dev", "/dev"}
|
|
for _, m := range mounts {
|
|
if m == "" {
|
|
continue
|
|
}
|
|
argv = append(argv, "--ro-bind", m, m)
|
|
}
|
|
argv = append(argv, "--clearenv")
|
|
for _, kv := range doctorEnv(pyPath) {
|
|
if i := strings.IndexByte(kv, '='); i > 0 {
|
|
argv = append(argv, "--setenv", kv[:i], kv[i+1:])
|
|
}
|
|
}
|
|
argv = append(argv, python, "-c", script)
|
|
cmd := exec.CommandContext(ctx, bwrap, argv...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("execenv: doctor sandbox: %v: %s", err, trim(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doctorEnv(pyPath []string) []string {
|
|
env := []string{
|
|
"PATH=/usr/local/bin:/usr/bin:/bin",
|
|
"PYTHONDONTWRITEBYTECODE=1",
|
|
"PIP_NO_INDEX=1",
|
|
}
|
|
if len(pyPath) > 0 {
|
|
env = append(env, "PYTHONPATH="+strings.Join(pyPath, string(os.PathListSeparator)))
|
|
}
|
|
return env
|
|
}
|
|
|
|
func trim(b []byte) string {
|
|
s := string(bytes.TrimSpace(b))
|
|
if len(s) > 500 {
|
|
s = s[:500]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// EnsurePython 优先下载独立 CPython;失败则用系统 python 建一份 venv。
|
|
func (r *Root) EnsurePython(ctx context.Context) (string, error) {
|
|
if bin, ok := r.PythonBin(); ok {
|
|
return bin, nil
|
|
}
|
|
pctx, cancel := context.WithTimeout(ctx, 4*time.Minute)
|
|
defer cancel()
|
|
if err := r.InstallStandalone(pctx, ""); err == nil {
|
|
if bin, ok := r.PythonBin(); ok {
|
|
return bin, nil
|
|
}
|
|
}
|
|
if err := r.BootstrapVenv("/usr/bin/python3"); err != nil {
|
|
return "", err
|
|
}
|
|
bin, ok := r.PythonBin()
|
|
if !ok {
|
|
return "", fmt.Errorf("execenv: managed python missing after bootstrap")
|
|
}
|
|
return bin, nil
|
|
}
|