Files
Agent-System-Backend/internal/app/execenv_wire.go
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

133 lines
4.6 KiB
Go

package app
import (
"agent-system-backend/internal/execenv"
"agent-system-backend/internal/store"
"agent-system-backend/internal/tools"
core "agent-system-core"
)
// envStore 把 store 的行类型适配成 execenv.Store。
type envStore struct{ *store.Store }
func (s envStore) InsertExecEnvRequest(r execenv.Request) error {
return s.Store.InsertExecEnvRequest(store.ExecEnvRequest{
ID: r.ID, ConversationID: r.ConversationID, Owner: r.Owner, Spec: r.Spec,
Reason: r.Reason, Status: r.Status, LayerID: r.LayerID, Detail: r.Detail,
Approver: r.Approver, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
}
func (s envStore) GetExecEnvRequest(id string) (execenv.Request, error) {
r, err := s.Store.GetExecEnvRequest(id)
if err != nil {
return execenv.Request{}, err
}
return execenv.Request{
ID: r.ID, ConversationID: r.ConversationID, Owner: r.Owner, Spec: r.Spec,
Reason: r.Reason, Status: r.Status, LayerID: r.LayerID, Detail: r.Detail,
Approver: r.Approver, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
}, nil
}
func (s envStore) ListExecEnvRequests(conversationID string, limit int) ([]execenv.Request, error) {
rows, err := s.Store.ListExecEnvRequests(conversationID, limit)
if err != nil {
return nil, err
}
out := make([]execenv.Request, 0, len(rows))
for _, r := range rows {
out = append(out, execenv.Request{
ID: r.ID, ConversationID: r.ConversationID, Owner: r.Owner, Spec: r.Spec,
Reason: r.Reason, Status: r.Status, LayerID: r.LayerID, Detail: r.Detail,
Approver: r.Approver, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
})
}
return out, nil
}
func (s envStore) UpdateExecEnvRequest(id, status, layerID, detail, approver string) error {
return s.Store.UpdateExecEnvRequest(id, status, layerID, detail, approver)
}
func (s envStore) GetExecEnvBinding(conversationID string) (execenv.Binding, error) {
b, err := s.Store.GetExecEnvBinding(conversationID)
if err != nil {
return execenv.Binding{}, err
}
return execenv.Binding{
ConversationID: b.ConversationID, BaseID: b.BaseID, Extensions: b.Layers,
Isolated: b.Isolated, UpdatedAt: b.UpdatedAt,
}, nil
}
func (s envStore) PutExecEnvBinding(b execenv.Binding) error {
return s.Store.PutExecEnvBinding(store.ExecEnvBinding{
ConversationID: b.ConversationID, BaseID: b.BaseID, Layers: b.Extensions,
Isolated: b.Isolated, UpdatedAt: b.UpdatedAt,
})
}
func (s envStore) ListExecEnvBindings() ([]execenv.Binding, error) {
rows, err := s.Store.ListExecEnvBindings()
if err != nil {
return nil, err
}
out := make([]execenv.Binding, 0, len(rows))
for _, b := range rows {
out = append(out, execenv.Binding{
ConversationID: b.ConversationID, BaseID: b.BaseID, Extensions: b.Layers,
Isolated: b.Isolated, UpdatedAt: b.UpdatedAt,
})
}
return out, nil
}
func (s envStore) InsertExecEnvAudit(owner, action, actor, target, outcome, entry string) error {
return s.Store.InsertExecEnvAudit(owner, action, actor, target, outcome, entry)
}
// wireExecEnv 打开环境根,把基础层交给执行器,并接上申请代理。
// 两者都空时不改变现状(解释器仍固定为 /usr/bin/python3,避免 linuxbrew 路径)。
func wireExecEnv(st *store.Store, opt *Options) (*execenv.Broker, error) {
if opt.ExecEnvRoot == "" {
return nil, nil
}
root, err := execenv.Open(opt.ExecEnvRoot)
if err != nil {
return nil, err
}
if bin, ok := root.PythonBin(); ok {
tools.SetExecPython(bin, root.PythonPrefix())
}
tools.SetExecShimDir(root.BinDir())
if opt.ExecDepsDir == "" {
if id, err := root.CurrentBase(); err != nil {
return nil, err
} else if id != "" {
opt.ExecDepsDir = root.LayerDir(id)
}
}
py, _ := root.PythonBin()
if py == "" {
py = "/usr/bin/python3"
}
broker := execenv.NewBroker(root, envStore{st}, execenv.Pip{Python: py})
tools.SetEnvBroker(broker)
tools.SetExecLayers(broker.LayersFor)
tools.SetEnvApproval(func(owner, conv, requestID, detail string) error {
card := &core.HITLCard{
ID: core.ID("hitl_" + requestID), RunID: "execenv", ConversationID: core.ID(conv),
Question: "批准为这个会话安装额外的 Python 包?", Kind: core.HITLKindChoice,
Detail: "exec-env:" + requestID + "\n" + detail,
Choices: []core.HITLChoice{
{ID: "approve", Label: "批准", Description: detail, Impact: "只挂到这个会话,不改变基础层", Recommended: true},
{ID: "isolate", Label: "批准并使用隔离层", Description: "与基础层版本冲突时用这份独立闭包", Impact: "这个会话不再叠基础层"},
{ID: "reject", Label: "拒绝", Description: "不安装", Impact: "会话保持现状"},
},
}
return st.SaveHITLCard(owner, card)
})
return broker, nil
}