Product binaries now receive the layered Python environment.
This commit was merged in pull request #207.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"agent-system-backend/internal/execenv"
|
"agent-system-backend/internal/execenv"
|
||||||
@@ -137,6 +138,9 @@ func runDoctor(ctx context.Context, root *execenv.Root, py string, sandbox bool)
|
|||||||
if prefix := root.PythonPrefix(); prefix != "" {
|
if prefix := root.PythonPrefix(); prefix != "" {
|
||||||
mounts = append(mounts, prefix)
|
mounts = append(mounts, prefix)
|
||||||
}
|
}
|
||||||
|
if resolved, err := filepath.EvalSymlinks(py); err == nil {
|
||||||
|
py = resolved
|
||||||
|
}
|
||||||
if err := execenv.Doctor(ctx, py, pyPath, nil, sandbox, mounts); err != nil {
|
if err := execenv.Doctor(ctx, py, pyPath, nil, sandbox, mounts); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "env doctor:", err)
|
fmt.Fprintln(os.Stderr, "env doctor:", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ func (p Pip) Resolve(ctx context.Context, reqs []string, base Lock) (Result, err
|
|||||||
if err := os.WriteFile(cons, []byte(constraintText(base)), 0o644); err != nil {
|
if err := os.WriteFile(cons, []byte(constraintText(base)), 0o644); err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
args := []string{"-m", "pip", "install", "--dry-run", "--report", "-", "--only-binary=:all:", "--constraint", cons}
|
// --report 写文件。写到 stdout 时 pip 仍把 Collecting / Would install 打在同一条流上,JSON 解析会失败。
|
||||||
|
report := filepath.Join(dir, "report.json")
|
||||||
|
args := []string{"-m", "pip", "install", "--dry-run", "--report", report, "--only-binary=:all:", "--constraint", cons}
|
||||||
args = append(args, reqs...)
|
args = append(args, reqs...)
|
||||||
cmd := exec.CommandContext(ctx, p.python(), args...)
|
cmd := exec.CommandContext(ctx, p.python(), args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -70,7 +72,11 @@ func (p Pip) Resolve(ctx context.Context, reqs []string, base Lock) (Result, err
|
|||||||
}
|
}
|
||||||
return Result{}, fmt.Errorf("execenv: pip resolve: %v: %s", err, firstLine(text))
|
return Result{}, fmt.Errorf("execenv: pip resolve: %v: %s", err, firstLine(text))
|
||||||
}
|
}
|
||||||
return parsePipReport(stdout.Bytes(), base)
|
raw, err := os.ReadFile(report)
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, fmt.Errorf("execenv: pip report: %w", err)
|
||||||
|
}
|
||||||
|
return parsePipReport(raw, base)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePipReport(raw []byte, base Lock) (Result, error) {
|
func parsePipReport(raw []byte, base Lock) (Result, error) {
|
||||||
|
|||||||
@@ -203,7 +203,13 @@ func resolveInterpreter(name string) (string, error) {
|
|||||||
case "python3":
|
case "python3":
|
||||||
if execPythonBin != "" {
|
if execPythonBin != "" {
|
||||||
if st, err := os.Stat(execPythonBin); err == nil && !st.IsDir() {
|
if st, err := os.Stat(execPythonBin); err == nil && !st.IsDir() {
|
||||||
return execPythonBin, nil
|
// current/bin/python3 是符号链接。沙箱用 tmpfs 盖住 /home 后只挂真实前缀,
|
||||||
|
// 执行未解析的 current 路径会找不到文件。
|
||||||
|
resolved, err := filepath.EvalSymlinks(execPythonBin)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resolved, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("python3: configured interpreter missing: %s", execPythonBin)
|
return "", fmt.Errorf("python3: configured interpreter missing: %s", execPythonBin)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,28 @@ func TestExecEnvIncludesSkillsRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveInterpreterFollowsManagedSymlink(t *testing.T) {
|
||||||
|
prev := execPythonBin
|
||||||
|
t.Cleanup(func() { execPythonBin = prev })
|
||||||
|
dir := t.TempDir()
|
||||||
|
link := filepath.Join(dir, "python3")
|
||||||
|
if err := os.Symlink("/usr/bin/python3", link); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
execPythonBin = link
|
||||||
|
got, err := resolveInterpreter("python3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want, err := filepath.EvalSymlinks("/usr/bin/python3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("resolved %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInterpreterExecConfigForKernel(t *testing.T) {
|
func TestInterpreterExecConfigForKernel(t *testing.T) {
|
||||||
cfg := InterpreterExecConfig()
|
cfg := InterpreterExecConfig()
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ type Options struct {
|
|||||||
// 仅会话工作目录与技能目录挂载、进程数限额、超时杀整个沙箱)。
|
// 仅会话工作目录与技能目录挂载、进程数限额、超时杀整个沙箱)。
|
||||||
// DisableExecTools 优先级高于此选择。
|
// DisableExecTools 优先级高于此选择。
|
||||||
ExecBackend string
|
ExecBackend string
|
||||||
|
// ExecDepsDir、ExecWorkspace、ExecEnvRoot 与基座 serve 同名。
|
||||||
|
// 产品入口不解析这三项旗标时留空,link 改读同名环境变量
|
||||||
|
//(AGENT_SYSTEM_EXEC_DEPS_DIR / AGENT_SYSTEM_EXEC_WORKSPACE / AGENT_SYSTEM_EXEC_ENV_ROOT)。
|
||||||
|
ExecDepsDir string
|
||||||
|
ExecWorkspace string
|
||||||
|
ExecEnvRoot string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -3,19 +3,37 @@
|
|||||||
package link
|
package link
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"agent-system-backend/internal/app"
|
"agent-system-backend/internal/app"
|
||||||
"agent-system-backend/pkg/extension"
|
"agent-system-backend/pkg/extension"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
extension.SetRunner(func(opt extension.Options) error {
|
extension.SetRunner(func(opt extension.Options) error {
|
||||||
return app.Run(app.Options{
|
return app.Run(appOptions(opt))
|
||||||
Addr: opt.Addr, DataDir: opt.DataDir, PluginsDir: opt.PluginsDir, ForceMock: opt.Mock,
|
|
||||||
StorageDSN: opt.StorageDSN, ArtifactsDir: opt.ArtifactsDir, AuthMode: opt.AuthMode,
|
|
||||||
DisableUploadIndexing: opt.DisableUploadIndexing,
|
|
||||||
DisableConvCardMirror: opt.DisableConvCardMirror,
|
|
||||||
DisableExecTools: opt.DisableExecTools,
|
|
||||||
ExecBackend: opt.ExecBackend,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// appOptions 把产品启动参数映射到基座。执行环境三项产品旗标留空时读环境变量,
|
||||||
|
// 与 cmd/agent-system 的 config 键一致,这样 svc 注入环境即可,不必改产品 main。
|
||||||
|
func appOptions(opt extension.Options) app.Options {
|
||||||
|
return app.Options{
|
||||||
|
Addr: opt.Addr, DataDir: opt.DataDir, PluginsDir: opt.PluginsDir, ForceMock: opt.Mock,
|
||||||
|
StorageDSN: opt.StorageDSN, ArtifactsDir: opt.ArtifactsDir, AuthMode: opt.AuthMode,
|
||||||
|
DisableUploadIndexing: opt.DisableUploadIndexing,
|
||||||
|
DisableConvCardMirror: opt.DisableConvCardMirror,
|
||||||
|
DisableExecTools: opt.DisableExecTools,
|
||||||
|
ExecBackend: opt.ExecBackend,
|
||||||
|
ExecDepsDir: firstSet(opt.ExecDepsDir, os.Getenv("AGENT_SYSTEM_EXEC_DEPS_DIR")),
|
||||||
|
ExecWorkspace: firstSet(opt.ExecWorkspace, os.Getenv("AGENT_SYSTEM_EXEC_WORKSPACE")),
|
||||||
|
ExecEnvRoot: firstSet(opt.ExecEnvRoot, os.Getenv("AGENT_SYSTEM_EXEC_ENV_ROOT")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstSet(explicit, fromEnv string) string {
|
||||||
|
if explicit != "" {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
return fromEnv
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"agent-system-backend/pkg/extension"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppOptionsPrefersExplicitExecPaths(t *testing.T) {
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_DEPS_DIR", "/from/env/deps")
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_WORKSPACE", "overlay")
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_ENV_ROOT", "/from/env/root")
|
||||||
|
got := appOptions(extension.Options{
|
||||||
|
ExecDepsDir: "/explicit/deps",
|
||||||
|
ExecWorkspace: "direct",
|
||||||
|
ExecEnvRoot: "/explicit/root",
|
||||||
|
})
|
||||||
|
if got.ExecDepsDir != "/explicit/deps" || got.ExecWorkspace != "direct" || got.ExecEnvRoot != "/explicit/root" {
|
||||||
|
t.Fatalf("explicit paths lost: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppOptionsReadsExecEnvWhenUnset(t *testing.T) {
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_DEPS_DIR", "/from/env/deps")
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_WORKSPACE", "overlay")
|
||||||
|
t.Setenv("AGENT_SYSTEM_EXEC_ENV_ROOT", "/from/env/root")
|
||||||
|
got := appOptions(extension.Options{ExecBackend: "sandbox"})
|
||||||
|
if got.ExecBackend != "sandbox" || got.ExecDepsDir != "/from/env/deps" || got.ExecWorkspace != "overlay" || got.ExecEnvRoot != "/from/env/root" {
|
||||||
|
t.Fatalf("env fallback: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user