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>
150 lines
4.8 KiB
Go
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
|
|
}
|