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:
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user