Why: A step confirmation cancelled the whole run, approvals and repair budget vanished on restart, and the HTTP server accepted anonymous and oversized requests. Evidence: GOWORK=off go vet ./... && GOWORK=off go test -count=1 ./... && GOWORK=off go test -race ./squad/... passed. scripts/submodule-consistency.sh ok. Impact: Approve and skip apply to one step; runstate.json and unix flock survive restart; wakeup leaves permission holds pending; serve defaults to loopback and requires a token off-loopback. Refs: Q1 Q2 Q3 Q4 Q5 Q8 Q9 Q14 Q15 Q16 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
// Package store 提供 agent-squad 的文件持久化:
|
|
// roles.d/*.json、blueprints.d/*.json;运行状态由引擎直接管理 runs/<id>/。
|
|
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"agent-squad/squad"
|
|
)
|
|
|
|
// FileStore 数据目录级文件存储。
|
|
type FileStore struct {
|
|
DataDir string
|
|
}
|
|
|
|
// New 创建文件存储并确保目录存在。
|
|
func New(dataDir string) (*FileStore, error) {
|
|
s := &FileStore{DataDir: dataDir}
|
|
for _, d := range []string{filepath.Join(dataDir, "roles.d"), filepath.Join(dataDir, "blueprints.d")} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// LoadRoles 加载 roles.d/*.json 到注册表,并挂接增量落盘钩子。
|
|
// 文件名(去扩展名)不作为权威,以 JSON 内 role 字段为准。
|
|
func (s *FileStore) LoadRoles(reg *squad.RoleRegistry) error {
|
|
entries, err := os.ReadDir(filepath.Join(s.DataDir, "roles.d"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, ent := range entries {
|
|
if ent.IsDir() || (!strings.HasSuffix(ent.Name(), ".json")) {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(s.DataDir, "roles.d", ent.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var spec squad.RoleSpec
|
|
if err := json.Unmarshal(data, &spec); err != nil {
|
|
return fmt.Errorf("roles.d/%s: %w", ent.Name(), err)
|
|
}
|
|
if err := reg.Register(spec); err != nil {
|
|
return fmt.Errorf("roles.d/%s: %w", ent.Name(), err)
|
|
}
|
|
}
|
|
reg.OnChange = s.SaveRole
|
|
return nil
|
|
}
|
|
|
|
// SaveRole 落盘单个角色。
|
|
func (s *FileStore) SaveRole(spec squad.RoleSpec) error {
|
|
data, err := json.MarshalIndent(spec, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return squad.AtomicWrite(filepath.Join(s.DataDir, "roles.d", spec.Role+".json"), append(data, '\n'))
|
|
}
|
|
|
|
// DeleteRole 删除角色文件(注册表删除由调用方完成)。
|
|
func (s *FileStore) DeleteRole(key string) {
|
|
_ = os.Remove(filepath.Join(s.DataDir, "roles.d", key+".json"))
|
|
}
|
|
|
|
// LoadBlueprints 加载 blueprints.d/*.json 并挂接落盘钩子。
|
|
func (s *FileStore) LoadBlueprints(bps *squad.BlueprintRegistry) error {
|
|
entries, err := os.ReadDir(filepath.Join(s.DataDir, "blueprints.d"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, ent := range entries {
|
|
if ent.IsDir() || !strings.HasSuffix(ent.Name(), ".json") {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(s.DataDir, "blueprints.d", ent.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var bp squad.Blueprint
|
|
if err := json.Unmarshal(data, &bp); err != nil {
|
|
return fmt.Errorf("blueprints.d/%s: %w", ent.Name(), err)
|
|
}
|
|
if err := bps.Register(bp); err != nil {
|
|
return fmt.Errorf("blueprints.d/%s: %w", ent.Name(), err)
|
|
}
|
|
}
|
|
bps.OnChange = s.SaveBlueprint
|
|
return nil
|
|
}
|
|
|
|
// SaveBlueprint 落盘单个蓝图。
|
|
func (s *FileStore) SaveBlueprint(bp squad.Blueprint) error {
|
|
data, err := json.MarshalIndent(bp, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return squad.AtomicWrite(filepath.Join(s.DataDir, "blueprints.d", bp.Key+".json"), append(data, '\n'))
|
|
}
|
|
|
|
// DeleteBlueprint 删除蓝图文件。
|
|
func (s *FileStore) DeleteBlueprint(key string) {
|
|
_ = os.Remove(filepath.Join(s.DataDir, "blueprints.d", key+".json"))
|
|
}
|