diff --git a/cmd/agent-system/env.go b/cmd/agent-system/env.go index 818d51d..845169b 100644 --- a/cmd/agent-system/env.go +++ b/cmd/agent-system/env.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "os" + "path/filepath" "time" "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 != "" { 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 { fmt.Fprintln(os.Stderr, "env doctor:", err) return 1 diff --git a/internal/execenv/pip.go b/internal/execenv/pip.go index 8125ce5..b2079d9 100644 --- a/internal/execenv/pip.go +++ b/internal/execenv/pip.go @@ -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 { 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...) cmd := exec.CommandContext(ctx, p.python(), args...) 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 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) { diff --git a/internal/tools/exec_interpreter.go b/internal/tools/exec_interpreter.go index 55f2917..d44aa9f 100644 --- a/internal/tools/exec_interpreter.go +++ b/internal/tools/exec_interpreter.go @@ -203,7 +203,13 @@ func resolveInterpreter(name string) (string, error) { case "python3": if execPythonBin != "" { 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) } diff --git a/internal/tools/exec_interpreter_test.go b/internal/tools/exec_interpreter_test.go index da0aa29..90b4364 100644 --- a/internal/tools/exec_interpreter_test.go +++ b/internal/tools/exec_interpreter_test.go @@ -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) { cfg := InterpreterExecConfig() if cfg == nil { diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index f28df47..d43a6ea 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -213,6 +213,12 @@ type Options struct { // 仅会话工作目录与技能目录挂载、进程数限额、超时杀整个沙箱)。 // DisableExecTools 优先级高于此选择。 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 ( diff --git a/pkg/extension/link/link.go b/pkg/extension/link/link.go index 1d6a41f..2419932 100644 --- a/pkg/extension/link/link.go +++ b/pkg/extension/link/link.go @@ -3,19 +3,37 @@ package link import ( + "os" + "agent-system-backend/internal/app" "agent-system-backend/pkg/extension" ) func init() { extension.SetRunner(func(opt extension.Options) error { - return app.Run(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, - }) + return app.Run(appOptions(opt)) }) } + +// 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 +} diff --git a/pkg/extension/link/link_test.go b/pkg/extension/link/link_test.go new file mode 100644 index 0000000..ebc0dcd --- /dev/null +++ b/pkg/extension/link/link_test.go @@ -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) + } +}