Why: identity ensure/verify went through the normal store open, which creates a database and runs migrations. A deploy gate that calls the candidate before stopping the old service could migrate production ahead of the switch, and a missing database was turned into an empty one. Evidence: go test ./pkg/identity/ ./internal/store/ ./cmd/agent-system/ -run 'TestCapabilities|TestEnsure|TestVerify|TestDSN|TestUsage|TestSystemIdentity|TestOpenIdentity|TestUnknownSubcommand' passed in this worktree. Impact: identity CLI refuses a missing file or a missing agent_system_identity table, does not migrate, reads DSN from the environment or a 0600 file, and version --json plus identity capabilities emit one capability document. Unknown top-level commands exit 2. Refs: recovery plan r3.1 D1; Dev feature/svc-d1-identity-flow Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
4.3 KiB
Go
134 lines
4.3 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")
|
||
|
||
// ErrIdentityDBMissing 身份 CLI 拒绝创建数据库文件或父目录。
|
||
var ErrIdentityDBMissing = errors.New("identity database missing")
|
||
|
||
// ErrIdentityTableMissing 库存在但没有冻结的身份表,须走受控迁移而不是身份 CLI。
|
||
var ErrIdentityTableMissing = errors.New("identity table missing")
|
||
|
||
// IdentityColumns 是 agent_system_identity 的冻结列序(v27)。新增列会破坏旧制品读取,必须先改本常量并补兼容测试。
|
||
const IdentityColumns = "instance_id,dbref,schema_version,created_at"
|
||
|
||
// 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, err := s.SchemaVersion()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if ver <= 0 {
|
||
ver = 27
|
||
}
|
||
_, 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
|
||
}
|