Files
Agent-Squad/store/store.go
weichenandCursor 8e987bf535
ci / vet-and-test (pull_request) Failing after 1s
ci / docker-checkout-hostgateway (pull_request) Failing after 3s
Persist step approvals and guard squad HTTP entry points
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>
2026-10-03 01:48:10 +00:00

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"))
}