Files
Agent-System/internal/sysadmin/store.go
T
weichenandCursor a2cbaf1e33 feat(sysadmin): CAS author-only policy with transactional audit
Register the workflow total switch, require expected_revision for
updates, and expose AuthorOnlyPolicySource for activation locking.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 09:15:15 +00:00

142 lines
4.9 KiB
Go

package sysadmin
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// ErrSettingNotFound DB 中无该 key 的行(回退 env/default)。
var ErrSettingNotFound = errors.New("sysadmin: setting not found")
// ErrSettingRevisionConflict CAS 期望 revision 与当前不一致。
var ErrSettingRevisionConflict = errors.New("sysadmin: setting revision conflict")
// SettingsStore system_settings 表读写(表 owner 为 sysadmin)。
type SettingsStore interface {
Get(ctx context.Context, key string) (value json.RawMessage, updatedBy *int64, updatedAt *time.Time, revision int64, err error)
Upsert(ctx context.Context, key string, value json.RawMessage, updatedBy int64) (updatedAt time.Time, revision int64, err error)
// UpsertCAS 在同一事务内校验 expectedRevision、写入新值并追加策略审计;冲突返回 ErrSettingRevisionConflict。
UpsertCAS(ctx context.Context, key string, value json.RawMessage, updatedBy, expectedRevision int64, requestID string) (updatedAt time.Time, revision int64, err error)
// GetForUpdate 在调用方事务内锁定目标行(FOR UPDATE),供激活路径与关闭串行化。
GetForUpdate(ctx context.Context, tx pgx.Tx, key string) (value json.RawMessage, updatedBy *int64, updatedAt *time.Time, revision int64, err error)
}
type pgSettingsStore struct {
pool *pgxpool.Pool
}
func newPGSettingsStore(pool *pgxpool.Pool) SettingsStore {
return &pgSettingsStore{pool: pool}
}
func (s *pgSettingsStore) Get(ctx context.Context, key string) (json.RawMessage, *int64, *time.Time, int64, error) {
var raw []byte
var updatedBy *int64
var updatedAt time.Time
var revision int64
err := s.pool.QueryRow(ctx, `
SELECT value, updated_by, updated_at, revision FROM system_settings WHERE key = $1
`, key).Scan(&raw, &updatedBy, &updatedAt, &revision)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil, nil, 0, ErrSettingNotFound
}
return nil, nil, nil, 0, err
}
return json.RawMessage(raw), updatedBy, &updatedAt, revision, nil
}
func (s *pgSettingsStore) Upsert(ctx context.Context, key string, value json.RawMessage, updatedBy int64) (time.Time, int64, error) {
var updatedAt time.Time
var revision int64
err := s.pool.QueryRow(ctx, `
INSERT INTO system_settings (key, value, updated_by, updated_at, revision)
VALUES ($1, $2, $3, now(), 0)
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, updated_by = EXCLUDED.updated_by, updated_at = now()
RETURNING updated_at, revision
`, key, []byte(value), updatedBy).Scan(&updatedAt, &revision)
return updatedAt, revision, err
}
func (s *pgSettingsStore) UpsertCAS(ctx context.Context, key string, value json.RawMessage, updatedBy, expectedRevision int64, requestID string) (time.Time, int64, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return time.Time{}, 0, err
}
defer tx.Rollback(ctx)
var beforeRaw []byte
var beforeRev int64
err = tx.QueryRow(ctx, `
SELECT value, revision FROM system_settings WHERE key = $1 FOR UPDATE
`, key).Scan(&beforeRaw, &beforeRev)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return time.Time{}, 0, ErrSettingNotFound
}
return time.Time{}, 0, err
}
if beforeRev != expectedRevision {
return time.Time{}, 0, ErrSettingRevisionConflict
}
var updatedAt time.Time
var newRev int64
err = tx.QueryRow(ctx, `
UPDATE system_settings
SET value = $2, updated_by = $3, updated_at = now(), revision = revision + 1
WHERE key = $1 AND revision = $4
RETURNING updated_at, revision
`, key, []byte(value), updatedBy, expectedRevision).Scan(&updatedAt, &newRev)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return time.Time{}, 0, ErrSettingRevisionConflict
}
return time.Time{}, 0, err
}
var reqID any
if requestID != "" {
reqID = requestID
}
_, err = tx.Exec(ctx, `
INSERT INTO system_setting_policy_audits (
setting_key, before_value, after_value, before_revision, after_revision, actor_user_id, request_id
) VALUES ($1, $2::jsonb, $3::jsonb, $4, $5, $6, $7)
`, key, beforeRaw, []byte(value), beforeRev, newRev, updatedBy, reqID)
if err != nil {
return time.Time{}, 0, err
}
if err := tx.Commit(ctx); err != nil {
return time.Time{}, 0, err
}
return updatedAt, newRev, nil
}
func (s *pgSettingsStore) GetForUpdate(ctx context.Context, tx pgx.Tx, key string) (json.RawMessage, *int64, *time.Time, int64, error) {
if tx == nil {
return nil, nil, nil, 0, errors.New("sysadmin: GetForUpdate requires transaction")
}
var raw []byte
var updatedBy *int64
var updatedAt time.Time
var revision int64
err := tx.QueryRow(ctx, `
SELECT value, updated_by, updated_at, revision FROM system_settings WHERE key = $1 FOR UPDATE
`, key).Scan(&raw, &updatedBy, &updatedAt, &revision)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil, nil, 0, ErrSettingNotFound
}
return nil, nil, nil, 0, err
}
return json.RawMessage(raw), updatedBy, &updatedAt, revision, nil
}