ci / vet-and-test (pull_request) Successful in 49s
Why: Concurrent password changes, recovery key reissue, and session admission could accept retired credentials or omit rejected audits. Evidence: Local Go vet and full tests passed; real PostgreSQL store suite, deterministic interleavings, and two-process contention passed with SELF receipts. Impact: Account writes and session admission serialize per owner; stale sessions fail closed; checked rejections carry an audit row. Refs: Agent-System-Dev #486; Agent-System-Backend #185; merge queue Issue #518
83 lines
2.8 KiB
Go
83 lines
2.8 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// dialect 收敛 SQLite / PostgreSQL 差异。全部查询以 ? 占位符书写,PG 执行前 rebind。
|
|
type dialect interface {
|
|
name() string
|
|
rebind(query string) string
|
|
// lockConversation 在事务内锁定会话的事件序列分配(跨实例安全)。
|
|
// PG:事务级咨询锁;SQLite:单写连接天然串行,无需额外锁。
|
|
lockConversation(tx *sql.Tx, conversationID string) error
|
|
// lockScope 锁定设置 scope 的读改写(owner+scope)。
|
|
lockScope(tx *sql.Tx, owner, scope string) error
|
|
// lockAdmission 锁定执行许可账本的清点与登记(U3 admission)。
|
|
lockAdmission(tx *sql.Tx) error
|
|
// lockBootstrap 锁定首次管理员引导的「核验无用户→建号→激活 Key」串行段
|
|
//(KTD12:防两个进程并发引导出双管理员)。
|
|
lockBootstrap(tx *sql.Tx) error
|
|
// lockAccount serializes credential and session admission for one owner.
|
|
lockAccount(tx *sql.Tx, owner string) error
|
|
}
|
|
|
|
// ---- SQLite(嵌入式档)----
|
|
|
|
type sqliteDialect struct{}
|
|
|
|
func (sqliteDialect) name() string { return "sqlite" }
|
|
func (sqliteDialect) rebind(q string) string { return q }
|
|
func (sqliteDialect) lockConversation(*sql.Tx, string) error { return nil }
|
|
func (sqliteDialect) lockScope(*sql.Tx, string, string) error { return nil }
|
|
func (sqliteDialect) lockAdmission(*sql.Tx) error { return nil }
|
|
func (sqliteDialect) lockBootstrap(*sql.Tx) error { return nil }
|
|
func (sqliteDialect) lockAccount(*sql.Tx, string) error { return nil }
|
|
|
|
// ---- PostgreSQL(多用户/多实例档)----
|
|
|
|
type postgresDialect struct{}
|
|
|
|
func (postgresDialect) name() string { return "postgres" }
|
|
|
|
func (postgresDialect) rebind(q string) string {
|
|
var b strings.Builder
|
|
n := 0
|
|
for i := 0; i < len(q); i++ {
|
|
if q[i] == '?' {
|
|
n++
|
|
fmt.Fprintf(&b, "$%d", n)
|
|
continue
|
|
}
|
|
b.WriteByte(q[i])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (postgresDialect) lockConversation(tx *sql.Tx, conversationID string) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "conv:"+conversationID)
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockScope(tx *sql.Tx, owner, scope string) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "scope:"+owner+"/"+scope)
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockAdmission(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "admission:exec-slots")
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockBootstrap(tx *sql.Tx) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "bootstrap:first-admin")
|
|
return err
|
|
}
|
|
|
|
func (postgresDialect) lockAccount(tx *sql.Tx, owner string) error {
|
|
_, err := tx.Exec(`SELECT pg_advisory_xact_lock(hashtext($1))`, "account:"+owner)
|
|
return err
|
|
}
|