Pass exec environment paths through the product entry
ci / vet-and-test (push) Successful in 59s

Product binaries now receive the layered Python environment.
This commit was merged in pull request #207.
This commit is contained in:
2026-10-15 14:44:48 +00:00
7 changed files with 104 additions and 11 deletions
+4
View File
@@ -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
+8 -2
View File
@@ -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) {
+7 -1
View File
@@ -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)
}
+22
View File
@@ -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 {
+6
View File
@@ -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 (
+26 -8
View File
@@ -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
}
+31
View File
@@ -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)
}
}