Files
weichenandCursor b85a1e1ca3 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>
2026-10-15 13:52:48 +00:00

125 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"time"
"agent-system-backend/internal/execenv"
core "agent-system-core"
)
// envBroker 由组合根注入。未注入时 python_env 明确报错,不假装能 pip install。
var envBroker *execenv.Broker
// envApproval 在申请需要人批时写一张选择卡。空则只把 pending 文本回给模型。
var envApproval func(owner, conversationID, requestID, detail string) error
// SetEnvBroker 注入分层环境代理。
func SetEnvBroker(b *execenv.Broker) { envBroker = b }
// SetEnvApproval 注入审批卡写入。
func SetEnvApproval(fn func(owner, conversationID, requestID, detail string) error) {
envApproval = fn
}
func registerPythonEnvTool(r *Registry) error {
return r.Register(core.ToolDesc{
Name: "python_env", Source: core.SourceBuiltin, Risk: "medium",
Description: "向系统申请 Python 包,或查看本会话已经能用的环境。沙箱不能联网安装。action=status 查看基础层与本会话的层;action=request 申请包(packages)或登记工作区内的 wheel;白名单内的包会自动装好并在下一次 run_python 生效,其余会进入审批。action=wait 等待 requestId 结束。不要用 pip install。",
Parameters: json.RawMessage(`{"type":"object","properties":{` +
`"action":{"type":"string","enum":["status","request","wait"],"description":"status | request | wait"},` +
`"packages":{"type":"array","items":{"type":"string"},"description":"包规格,如 pdfplumber 或 numpy==2.2.0"},` +
`"reason":{"type":"string","description":"为什么需要这些包"},` +
`"wheel":{"type":"string","description":"工作区内的 .whl 相对路径,上传的包需要审批"},` +
`"request_id":{"type":"string","description":"wait 时的申请 ID"},` +
`"timeout_sec":{"type":"integer","description":"wait 的秒数,默认 120"}}` +
`,"required":["action"]}`),
}, execPythonEnv)
}
func execPythonEnv(rc ToolExecCtx, args json.RawMessage) (string, error) {
if err := execGate(rc); err != nil {
return "", err
}
if envBroker == nil {
return "", fmt.Errorf("python_env: 环境代理未配置(启动参数 execEnvRoot)")
}
var a struct {
Action string `json:"action"`
Packages []string `json:"packages"`
Reason string `json:"reason"`
Wheel string `json:"wheel"`
RequestID string `json:"request_id"`
TimeoutS int `json:"timeout_sec"`
}
if len(args) > 0 {
if err := json.Unmarshal(args, &a); err != nil {
return "", fmt.Errorf("python_env: bad args: %w", err)
}
}
switch a.Action {
case "status":
return envBroker.StatusText(rc.ConversationID())
case "wait":
if strings.TrimSpace(a.RequestID) == "" {
return "", fmt.Errorf("python_env: request_id required")
}
timeout := time.Duration(a.TimeoutS) * time.Second
out, err := envBroker.Wait(execParentCtx(rc), a.RequestID, timeout)
if err != nil {
return "", err
}
raw, _ := json.Marshal(out)
return string(raw), nil
case "request":
wheel := ""
if strings.TrimSpace(a.Wheel) != "" {
sandbox, err := execSandbox(rc)
if err != nil {
return "", err
}
wheel, err = filepath.Abs(filepath.Join(sandbox, a.Wheel))
if err != nil {
return "", err
}
if !strings.HasPrefix(wheel, sandbox+string(filepath.Separator)) {
return "", fmt.Errorf("python_env: wheel must stay inside the workspace")
}
}
out, err := envBroker.RequestPackages(execParentCtx(rc), rc.ConversationID(), rc.OwnerID(), a.Reason, a.Packages, wheel)
if err != nil {
return "", err
}
if out.Status == "pending_approval" && envApproval != nil {
if err := envApproval(rc.OwnerID(), rc.ConversationID(), out.RequestID, out.Detail); err != nil {
out.Detail += ";审批卡未写入:" + err.Error()
}
}
raw, _ := json.Marshal(out)
return string(raw), nil
default:
return "", fmt.Errorf("python_env: unknown action %q", a.Action)
}
}
// ExecDoctor 用当前执行后端探针导入基础交付物库。依赖层已配置但导入失败时返回错误。
func ExecDoctor() error {
bin, err := resolveInterpreter("python3")
if err != nil {
return err
}
dirs, _ := pythonPathFor("")
mounts := append([]string{}, dirs...)
if execPythonPrefix != "" {
mounts = append(mounts, execPythonPrefix)
}
if execShimDir != "" {
mounts = append(mounts, execShimDir)
}
return execenv.Doctor(context.Background(), bin, dirs, nil, ExecBackendName() == "sandbox", mounts)
}