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>
This commit is contained in:
2026-10-15 13:52:48 +00:00
co-authored by Cursor
parent 24bbb7b777
commit b85a1e1ca3
38 changed files with 3112 additions and 52 deletions
+146
View File
@@ -0,0 +1,146 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"agent-system-backend/internal/execenv"
)
func runEnv(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "用法:agent-system env <ensure|lock|gc|doctor|rebase|python> --root <dir>")
return 2
}
sub := args[0]
fs := flag.NewFlagSet("env "+sub, flag.ExitOnError)
rootPath := fs.String("root", "", "环境根目录")
ttl := fs.Duration("ttl", 7*24*time.Hour, "gc 保留窗口")
bin := fs.String("bin", "", "python link 要指向的解释器")
_ = fs.Parse(args[1:])
if *rootPath == "" {
fmt.Fprintln(os.Stderr, "env: --root 必填")
return 2
}
root, err := execenv.Open(*rootPath)
if err != nil {
fmt.Fprintln(os.Stderr, "env:", err)
return 1
}
ctx := context.Background()
switch sub {
case "ensure":
py, err := root.EnsurePython(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "env ensure:", err)
return 1
}
id, err := execenv.NewBroker(root, nil, execenv.Pip{Python: py}).EnsureBase(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "env ensure:", err)
return 1
}
fmt.Println(id)
return runDoctor(ctx, root, py, true)
case "lock":
id, err := root.CurrentBase()
if err != nil || id == "" {
fmt.Fprintln(os.Stderr, "env lock: 没有 current 基础层")
return 1
}
lock, err := root.ReadLock(id)
if err != nil {
fmt.Fprintln(os.Stderr, "env lock:", err)
return 1
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(lock); err != nil {
fmt.Fprintln(os.Stderr, "env lock:", err)
return 1
}
return 0
case "gc":
removed, err := execenv.NewBroker(root, nil, execenv.Pip{}).GC(*ttl)
if err != nil {
fmt.Fprintln(os.Stderr, "env gc:", err)
return 1
}
for _, id := range removed {
fmt.Println(id)
}
return 0
case "doctor":
py, ok := root.PythonBin()
if !ok {
py = "/usr/bin/python3"
}
return runDoctor(ctx, root, py, true)
case "rebase":
py, err := root.EnsurePython(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "env rebase:", err)
return 1
}
id, err := execenv.NewBroker(root, nil, execenv.Pip{Python: py}).Rebase(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "env rebase:", err)
return 1
}
fmt.Println(id)
return 0
case "python":
action := "install"
if fs.NArg() > 0 {
action = fs.Arg(0)
}
switch action {
case "install":
py, err := root.EnsurePython(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, "env python:", err)
return 1
}
fmt.Println(py)
return 0
case "link":
if *bin == "" {
fmt.Fprintln(os.Stderr, "env python link: --bin 必填")
return 2
}
if err := root.LinkPython(*bin); err != nil {
fmt.Fprintln(os.Stderr, "env python link:", err)
return 1
}
fmt.Println(*bin)
return 0
default:
fmt.Fprintln(os.Stderr, "env python: install | link")
return 2
}
default:
fmt.Fprintf(os.Stderr, "env: 未知动作 %s\n", sub)
return 2
}
}
func runDoctor(ctx context.Context, root *execenv.Root, py string, sandbox bool) int {
var pyPath []string
if id, err := root.CurrentBase(); err == nil && id != "" {
pyPath = append(pyPath, root.LayerDir(id))
}
mounts := append([]string{}, pyPath...)
if prefix := root.PythonPrefix(); prefix != "" {
mounts = append(mounts, prefix)
}
if err := execenv.Doctor(ctx, py, pyPath, nil, sandbox, mounts); err != nil {
fmt.Fprintln(os.Stderr, "env doctor:", err)
return 1
}
fmt.Println("doctor ok")
return 0
}
+17 -5
View File
@@ -46,9 +46,11 @@ func main() {
os.Exit(runIdentity(os.Args[2:]))
case "capabilities":
os.Exit(runCapabilities(os.Args[2:]))
case "env":
os.Exit(runEnv(os.Args[2:]))
default:
fmt.Fprintf(os.Stderr, "agent-system: 未知子命令 %s\n", os.Args[1])
fmt.Fprintln(os.Stderr, "用法:agent-system <serve|version|identity|user|apikey|config|capabilities> [参数]")
fmt.Fprintln(os.Stderr, "用法:agent-system <serve|version|identity|user|apikey|config|capabilities|env> [参数]")
os.Exit(2)
}
}
@@ -68,6 +70,7 @@ func main() {
execBackend := fs.String("exec-backend", "", "执行后端:direct(默认,现状直启)| sandbox(bwrap 隔离档:断网、只读系统根、仅会话目录可写、进程数限额)")
execDepsDir := fs.String("exec-deps-dir", "", "sandbox 档只读依赖层目录(site-packages 等;空=不挂载)")
execWorkspace := fs.String("exec-workspace", "", "sandbox 档工作区模式:空=直写会话目录 | overlay=冻结基底+私有 upper+CAS 回收(S0)")
execEnvRoot := fs.String("exec-env-root", "", "分层 Python 环境根(解释器、基础层、扩展层;空=不启用)")
tier := fs.String("tier", "", "部署档:空=auto(DSN→PG,否则 SQLite)| local(SQLite,不允许 DSN)| team(PG fail-closed:缺 DSN/未认证拒绝启动,PG 故障拒绝就绪)")
pgMaxOpen := fs.Int("pg-max-open", 0, "PG 最大打开连接(0=默认 16)")
pgMaxIdle := fs.Int("pg-max-idle", 0, "PG 最大空闲连接(0=默认 8)")
@@ -93,7 +96,7 @@ func main() {
Addr: *addr, DataDir: *data, PluginsDir: *pluginsDir, Mock: *mock,
StorageDSN: *dsn, ArtifactsDir: *artifacts, AuthMode: *auth, Instance: *instance,
InsecureListen: *insecure, AllowedHostsRaw: *allowedHosts, DisableExecTools: *noExec,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace, ExecEnvRoot: *execEnvRoot,
Tier: *tier, PGMaxOpen: *pgMaxOpen, PGMaxIdle: *pgMaxIdle,
PGConnMaxIdleSec: *pgIdleSec, PGConnMaxLifetimeSec: *pgLifetimeSec, PGQueryTimeoutSec: *pgQueryTimeoutSec,
}, cliSet)
@@ -137,8 +140,8 @@ func main() {
DisableUploadIndexing: extOpt.DisableUploadIndexing,
DisableConvCardMirror: extOpt.DisableConvCardMirror,
DisableExecTools: extOpt.DisableExecTools, ExecBackend: extOpt.ExecBackend, ExecDepsDir: resolved.ExecDepsDir,
ExecWorkspace: resolved.ExecWorkspace,
Tier: resolved.Tier,
ExecWorkspace: resolved.ExecWorkspace, ExecEnvRoot: resolved.ExecEnvRoot,
Tier: resolved.Tier,
PGPool: store.PoolOptions{
MaxOpenConn: resolved.PGMaxOpen, MaxIdleConn: resolved.PGMaxIdle,
ConnMaxIdleTimeSec: resolved.PGConnMaxIdleSec, ConnMaxLifetimeSec: resolved.PGConnMaxLifetimeSec,
@@ -162,6 +165,14 @@ func visited(fs *flag.FlagSet) map[string]bool {
name = "allowedHosts"
case "disable-exec-tools":
name = "disableExecTools"
case "exec-backend":
name = "execBackend"
case "exec-deps-dir":
name = "execDepsDir"
case "exec-workspace":
name = "execWorkspace"
case "exec-env-root":
name = "execEnvRoot"
case "pg-max-open":
name = "pgMaxOpen"
case "pg-max-idle":
@@ -200,6 +211,7 @@ func runConfig(args []string) int {
execBackend := fs.String("exec-backend", "", "")
execDepsDir := fs.String("exec-deps-dir", "", "")
execWorkspace := fs.String("exec-workspace", "", "")
execEnvRoot := fs.String("exec-env-root", "", "")
tier := fs.String("tier", "", "")
pgMaxOpen := fs.Int("pg-max-open", 0, "")
pgMaxIdle := fs.Int("pg-max-idle", 0, "")
@@ -211,7 +223,7 @@ func runConfig(args []string) int {
Addr: *addr, DataDir: *data, PluginsDir: *pluginsDir, Mock: *mock,
StorageDSN: *dsn, ArtifactsDir: *artifacts, AuthMode: *auth, Instance: *instance,
InsecureListen: *insecure, AllowedHostsRaw: *allowedHosts, DisableExecTools: *noExec,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace,
ExecBackend: *execBackend, ExecDepsDir: *execDepsDir, ExecWorkspace: *execWorkspace, ExecEnvRoot: *execEnvRoot,
Tier: *tier, PGMaxOpen: *pgMaxOpen, PGMaxIdle: *pgMaxIdle,
PGConnMaxIdleSec: *pgIdleSec, PGConnMaxLifetimeSec: *pgLifetimeSec, PGQueryTimeoutSec: *pgQueryTimeoutSec,
}, visited(fs))