Files
Agent-System-Backend/internal/store/exec_env.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

150 lines
4.8 KiB
Go

package store
import (
"database/sql"
"encoding/json"
"errors"
)
// ExecEnvRequest 是一次扩展包申请(分层 Python 环境)。
type ExecEnvRequest struct {
ID string
ConversationID string
Owner string
Spec string
Reason string
Status string
LayerID string
Detail string
Approver string
CreatedAt string
UpdatedAt string
}
// ExecEnvBinding 是会话挂载的层。Layers 是扩展层 ID;Isolated 非空时替换基础层。
type ExecEnvBinding struct {
ConversationID string
BaseID string
Layers []string
Isolated string
UpdatedAt string
}
func (s *Store) InsertExecEnvRequest(r ExecEnvRequest) error {
_, err := s.db.Exec(s.q(`INSERT INTO exec_env_requests(
id,conversation_id,owner,spec,reason,status,layer_id,detail,approver,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`),
r.ID, r.ConversationID, r.Owner, r.Spec, r.Reason, r.Status, r.LayerID, r.Detail, r.Approver, r.CreatedAt, r.UpdatedAt)
return err
}
func (s *Store) GetExecEnvRequest(id string) (ExecEnvRequest, error) {
var r ExecEnvRequest
err := s.db.QueryRow(s.q(`SELECT id,conversation_id,owner,spec,reason,status,layer_id,detail,approver,created_at,updated_at
FROM exec_env_requests WHERE id=?`), id).Scan(
&r.ID, &r.ConversationID, &r.Owner, &r.Spec, &r.Reason, &r.Status, &r.LayerID, &r.Detail, &r.Approver, &r.CreatedAt, &r.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return ExecEnvRequest{}, ErrNotFound
}
return r, err
}
func (s *Store) ListExecEnvRequests(conversationID string, limit int) ([]ExecEnvRequest, error) {
if limit <= 0 {
limit = 20
}
q := `SELECT id,conversation_id,owner,spec,reason,status,layer_id,detail,approver,created_at,updated_at
FROM exec_env_requests`
var args []any
if conversationID != "" {
q += ` WHERE conversation_id=?`
args = append(args, conversationID)
}
q += ` ORDER BY created_at DESC LIMIT ?`
args = append(args, limit)
rows, err := s.db.Query(s.q(q), args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanExecEnvRequests(rows)
}
func scanExecEnvRequests(rows *sql.Rows) ([]ExecEnvRequest, error) {
var out []ExecEnvRequest
for rows.Next() {
var r ExecEnvRequest
if err := rows.Scan(&r.ID, &r.ConversationID, &r.Owner, &r.Spec, &r.Reason, &r.Status, &r.LayerID, &r.Detail, &r.Approver, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func (s *Store) UpdateExecEnvRequest(id, status, layerID, detail, approver string) error {
res, err := s.db.Exec(s.q(`UPDATE exec_env_requests SET status=?, layer_id=?, detail=?, approver=?, updated_at=? WHERE id=?`),
status, layerID, detail, approver, nowISO(), id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) GetExecEnvBinding(conversationID string) (ExecEnvBinding, error) {
var b ExecEnvBinding
var layers string
err := s.db.QueryRow(s.q(`SELECT conversation_id,base_id,layers,isolated,updated_at FROM exec_env_bindings WHERE conversation_id=?`), conversationID).
Scan(&b.ConversationID, &b.BaseID, &layers, &b.Isolated, &b.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return ExecEnvBinding{}, nil
}
if err != nil {
return ExecEnvBinding{}, err
}
_ = json.Unmarshal([]byte(layers), &b.Layers)
return b, nil
}
func (s *Store) PutExecEnvBinding(b ExecEnvBinding) error {
raw, err := json.Marshal(b.Layers)
if err != nil {
return err
}
_, err = s.db.Exec(s.q(`INSERT INTO exec_env_bindings(conversation_id,base_id,layers,isolated,updated_at)
VALUES(?,?,?,?,?)
ON CONFLICT(conversation_id) DO UPDATE SET base_id=excluded.base_id, layers=excluded.layers, isolated=excluded.isolated, updated_at=excluded.updated_at`),
b.ConversationID, b.BaseID, string(raw), b.Isolated, b.UpdatedAt)
return err
}
func (s *Store) ListExecEnvBindings() ([]ExecEnvBinding, error) {
rows, err := s.db.Query(s.q(`SELECT conversation_id,base_id,layers,isolated,updated_at FROM exec_env_bindings`))
if err != nil {
return nil, err
}
defer rows.Close()
var out []ExecEnvBinding
for rows.Next() {
var b ExecEnvBinding
var layers string
if err := rows.Scan(&b.ConversationID, &b.BaseID, &layers, &b.Isolated, &b.UpdatedAt); err != nil {
return nil, err
}
_ = json.Unmarshal([]byte(layers), &b.Layers)
out = append(out, b)
}
return out, rows.Err()
}
func (s *Store) InsertExecEnvAudit(owner, action, actor, target, outcome, entry string) error {
_, err := s.db.Exec(s.q(`INSERT INTO audit_events(time,owner,action,actor,target,evidence,trace_id,entry,instance_id,operation_id,outcome)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`),
nowISO(), owner, action, actor, target, entry, "", "execenv", s.instanceID, "", outcome)
return err
}