Files
Agent-System-Backend/internal/store/dialect.go
T
weichen bc1e0a36f7
ci / vet-and-test (pull_request) Successful in 51s
Close the account lifecycle leftovers: atomicity, bootstrap lock, redeem guard, credential delivery
Why: The login recovery contract leftovers and KTD12/KTD14 require:
concurrent same-name account creation must not overwrite the winner,
first-admin bootstrap must serialize across processes, the redeem
endpoint backoff must be shared across instances, revoked API keys
must close established SSE streams, and CLI credentials need an
env-DSN path plus a non-interactive 0600 file delivery mode.

Evidence: go test ./... green; new tests cover 16-way concurrent
create (1 win/15 conflict, winner credentials intact), 8-way
concurrent bootstrap (single admin, refuse takeover after), redeem
guard shared across two store handles on one DB, SSE stream closed
after key revocation within the recheck window, and HTTP 429 after
the failure cap (SELF worktree run; CI is the GATE, PG via test-pg).

Impact: Migration v29 (redeem_attempts); CreateAccount becomes a
single-row INSERT with unique-violation conflict; BootstrapFirstAdmin
advisory-locks (PG) / rechecks (SQLite) and commits admin+key+audit in
one transaction; SSE revalidation covers API-key streams; redeem
backoff moves to the shared DB; CLI gains AGENT_SYSTEM_DSN fallback
and --out file delivery for create/reset-password/issue-login-key/
bootstrap-admin.

Refs: docs/svc-deploy/2026-10-13-login-recovery-contract.md leftovers;
proposal r2 KTD12/KTD14/R5/R11.
2026-10-13 06:05:45 +00:00

75 lines
2.4 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
}
// ---- 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 }
// ---- 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
}