Why: U1 I3 needs a DB-side instance identity anchor matching manifest.instance_id so wrong/missing DB blocks startup (I4). Evidence: go test ./internal/store/ ./internal/app/; CLI smoke ensure/verify on SQLite temp dir. Impact: migration v26; store Ensure/Verify; identity subcommand; serve checks AGENT_SYSTEM_INSTANCE_ID when set. Refs: docs/recovery/u1-identity-manifest.md §5.2; Dev feature/recovery-u1-db-identity Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
package store
|
||
|
||
import (
|
||
"database/sql"
|
||
"errors"
|
||
"fmt"
|
||
)
|
||
|
||
// ErrSystemIdentityMismatch 库内身份与期望不符(错库/错实例/多行)——启动必须失败。
|
||
var ErrSystemIdentityMismatch = errors.New("system identity mismatch")
|
||
|
||
// ErrSystemIdentityMissing 表中无身份行——须先 Ensure(svc init / 首次建立),禁止静默插入。
|
||
var ErrSystemIdentityMissing = errors.New("system identity missing")
|
||
|
||
// SystemIdentity 是 agent_system_identity 表的一行(一库一服务身份)。
|
||
type SystemIdentity struct {
|
||
InstanceID string
|
||
DBRef string
|
||
SchemaVersion int
|
||
CreatedAt string
|
||
}
|
||
|
||
// EnsureSystemIdentity 仅用于显式建立路径:表空则插入;已存在且一致则幂等;
|
||
// 不一致或多余行 fail-closed。绝不在常规 start 路径调用以「补写」掩盖漂移。
|
||
func (s *Store) EnsureSystemIdentity(instanceID, dbref string) error {
|
||
if instanceID == "" {
|
||
return fmt.Errorf("EnsureSystemIdentity: instance_id 为空")
|
||
}
|
||
if dbref == "" {
|
||
dbref = "sqlite"
|
||
}
|
||
row, n, err := s.readSystemIdentity()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if n > 1 {
|
||
return fmt.Errorf("%w: agent_system_identity 有 %d 行(期望至多 1)", ErrSystemIdentityMismatch, n)
|
||
}
|
||
if n == 1 {
|
||
if row.InstanceID != instanceID || row.DBRef != dbref {
|
||
return fmt.Errorf("%w: 库内 instance_id=%q dbref=%q ≠ 期望 %q / %q",
|
||
ErrSystemIdentityMismatch, row.InstanceID, row.DBRef, instanceID, dbref)
|
||
}
|
||
return nil
|
||
}
|
||
ver := latestMigrationVersion()
|
||
_, err = s.db.Exec(s.q(`INSERT INTO agent_system_identity(instance_id, dbref, schema_version, created_at) VALUES(?,?,?,?)`),
|
||
instanceID, dbref, ver, nowISO())
|
||
if err != nil {
|
||
return fmt.Errorf("EnsureSystemIdentity insert: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// VerifySystemIdentity 常规启动校验:必须恰有一行且与期望一致;缺失不插入。
|
||
func (s *Store) VerifySystemIdentity(instanceID, dbref string) error {
|
||
if instanceID == "" {
|
||
return fmt.Errorf("VerifySystemIdentity: instance_id 为空")
|
||
}
|
||
if dbref == "" {
|
||
dbref = "sqlite"
|
||
}
|
||
row, n, err := s.readSystemIdentity()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if n == 0 {
|
||
return fmt.Errorf("%w: agent_system_identity 无行(先 svc init / identity ensure,禁止静默建空身份)", ErrSystemIdentityMissing)
|
||
}
|
||
if n > 1 {
|
||
return fmt.Errorf("%w: agent_system_identity 有 %d 行(期望至多 1)", ErrSystemIdentityMismatch, n)
|
||
}
|
||
if row.InstanceID != instanceID || row.DBRef != dbref {
|
||
return fmt.Errorf("%w: 库内 instance_id=%q dbref=%q ≠ 期望 %q / %q",
|
||
ErrSystemIdentityMismatch, row.InstanceID, row.DBRef, instanceID, dbref)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Store) readSystemIdentity() (SystemIdentity, int, error) {
|
||
rows, err := s.db.Query(`SELECT instance_id, dbref, schema_version, created_at FROM agent_system_identity`)
|
||
if err != nil {
|
||
return SystemIdentity{}, 0, fmt.Errorf("read agent_system_identity: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
var out SystemIdentity
|
||
n := 0
|
||
for rows.Next() {
|
||
n++
|
||
var id, ref, created string
|
||
var ver int
|
||
if err := rows.Scan(&id, &ref, &ver, &created); err != nil {
|
||
return SystemIdentity{}, 0, err
|
||
}
|
||
if n == 1 {
|
||
out = SystemIdentity{InstanceID: id, DBRef: ref, SchemaVersion: ver, CreatedAt: created}
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return SystemIdentity{}, 0, err
|
||
}
|
||
return out, n, nil
|
||
}
|
||
|
||
// GetSystemIdentity 读取唯一身份行(无行返回 sql.ErrNoRows)。
|
||
func (s *Store) GetSystemIdentity() (SystemIdentity, error) {
|
||
row, n, err := s.readSystemIdentity()
|
||
if err != nil {
|
||
return SystemIdentity{}, err
|
||
}
|
||
if n == 0 {
|
||
return SystemIdentity{}, sql.ErrNoRows
|
||
}
|
||
if n > 1 {
|
||
return SystemIdentity{}, fmt.Errorf("%w: %d rows", ErrSystemIdentityMismatch, n)
|
||
}
|
||
return row, nil
|
||
}
|