Files
Agent-System-Backend/internal/store/system_identity.go
T
weichenandCursor 69ea7dcc14 Open identity commands on an existing database only
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>
2026-10-12 14:04:41 +00:00

134 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}