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>
133 lines
4.6 KiB
Go
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
|
|
}
|