From 50d25ac690b54d108473260678bc21e34beafc57 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 14:35:46 +0000 Subject: [PATCH 1/3] Pass exec environment paths through the product entry Why: RDM starts the server through extension.Main, which dropped ExecDepsDir, ExecWorkspace, and ExecEnvRoot, so a deployed product ignored the layered Python environment. Evidence: go test ./pkg/extension/link/ passed on this worktree (SELF). Impact: a product binary reads the same AGENT_SYSTEM_EXEC_* variables as the base CLI when its own flags are empty; explicit Options still win. Refs: Agent-System-Dev PR 570 Co-authored-by: Cursor --- pkg/extension/extension.go | 6 ++++++ pkg/extension/link/link.go | 34 +++++++++++++++++++++++++-------- pkg/extension/link/link_test.go | 31 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 pkg/extension/link/link_test.go 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) + } +} From c5a4c6306bd0c0b537b880410a5ac5eade3eee7e Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 14:38:26 +0000 Subject: [PATCH 2/3] Write the pip dry-run report to a file Why: pip --report - mixes Collecting progress and the JSON document on stdout, so the first base-layer resolve failed before any wheel was stored. Evidence: go test ./internal/execenv/ passed on this worktree (SELF). A live dry-run of python-docx showed the progress text before the JSON object. Impact: resolve reads report.json and ignores the progress stream. Package selection is unchanged. Refs: Agent-System-Backend PR 207 Co-authored-by: Cursor --- internal/execenv/pip.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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) { From 28544cfe5da90e97474c42244ad8796e8029eed0 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 14:43:13 +0000 Subject: [PATCH 3/3] Execute the managed interpreter through its real path Why: the sandbox hides /home and only remounts the resolved Python prefix, so executing python/current/bin/python3 failed before the base layer could be imported. Evidence: go test ./internal/tools/ -run TestResolveInterpreterFollowsManagedSymlink passed, and env doctor against the built base layer printed doctor ok (SELF). Impact: run_python and env doctor follow the symlink into the mounted prefix or /usr/bin/python3. The layer and the prefix mount stay the same. Refs: Agent-System-Backend PR 207 Co-authored-by: Cursor --- cmd/agent-system/env.go | 4 ++++ internal/tools/exec_interpreter.go | 8 +++++++- internal/tools/exec_interpreter_test.go | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) 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/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 {