c8a87f1733
executus CI / test (pull_request) Successful in 1m34s
All 3 cloud models converged on real concurrency bugs in the SQLite stores: - AppendVersion (HIGH): the seq key was `SELECT MAX(seq)+1` then INSERT in two un-transacted statements with a NON-unique index, AND the Scan error was swallowed (seq stayed 0 on failure). Concurrent appends could both land the same seq, silently breaking newest-first ordering. Now: one transaction, the Scan error is propagated, the (skill_id, seq) index is UNIQUE (the loser of a race fails loudly), and an empty SkillID is rejected. - MarkScheduledRun / MarkAgentScheduledRun (all 3): replaced the Get→mutate→Save read-modify-write (lost-update window) with a single atomic UPDATE using json_set, so a concurrent Mark/edit can't clobber it. json_set keeps the JSON blob's NextRunAt/LastScheduledRunAt consistent with the indexed column; RFC3339Nano matches Go's time encoding so the blob still round-trips (tested). - Open: actually applies PRAGMA busy_timeout=5000 (the doc advertised it but it was never set) — a contended writer waits instead of erroring SQLITE_BUSY. - budgetStore.Add: rejects NaN/Inf secondsUsed (would irrecoverably poison the column). Triaged-but-kept: plaintext webhook secret (documented design, high-entropy URL key, pre-existing); SQL()/free-form `where` helpers (no untrusted input reaches them — defense-in-depth notes only). Core go.sum still free of host/DB deps; contrib/store green (incl. a json_set blob-round-trip test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.1 KiB
Go
55 lines
2.1 KiB
Go
// Package store provides durable, pure-Go SQLite implementations of executus's
|
|
// battery store seams (audit, budget, persona, skill). It is a SEPARATE nested
|
|
// module so the SQLite driver (modernc.org/sqlite — pure Go, no cgo) never
|
|
// enters the executus core go.sum: a static-binary host (gadfly) that imports
|
|
// only the core stays static, while a host that wants turnkey persistence
|
|
// imports this module and gets every *Store seam backed by one SQLite file.
|
|
//
|
|
// db, _ := store.Open("file:executus.db?_pragma=busy_timeout(5000)")
|
|
// defer db.Close()
|
|
// budgetStore := db.Budget() // satisfies budget.BudgetStorage
|
|
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
_ "modernc.org/sqlite" // pure-Go driver, registered as "sqlite"
|
|
)
|
|
|
|
// DB is a handle to one SQLite database backing the executus store seams. Each
|
|
// accessor (Budget(), …) returns a seam implementation sharing this connection.
|
|
// Safe for concurrent use (SQLite serializes writes; busy_timeout handles
|
|
// contention). Construct with Open; close with Close.
|
|
type DB struct {
|
|
sql *sql.DB
|
|
}
|
|
|
|
// Open opens (creating if absent) a SQLite database at dsn and returns a DB. A
|
|
// dsn of ":memory:" yields an ephemeral in-memory database. The caller owns the
|
|
// returned DB and must Close it.
|
|
func Open(dsn string) (*DB, error) {
|
|
sqldb, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store: open %q: %w", dsn, err)
|
|
}
|
|
// A contended writer should WAIT for the lock, not fail immediately — set a
|
|
// busy_timeout so concurrent stores don't see spurious SQLITE_BUSY. (The
|
|
// doc example advertised this; it's now actually applied for every DSN.)
|
|
if _, err := sqldb.Exec("PRAGMA busy_timeout=5000"); err != nil {
|
|
sqldb.Close()
|
|
return nil, fmt.Errorf("store: set busy_timeout %q: %w", dsn, err)
|
|
}
|
|
if err := sqldb.Ping(); err != nil {
|
|
sqldb.Close()
|
|
return nil, fmt.Errorf("store: ping %q: %w", dsn, err)
|
|
}
|
|
return &DB{sql: sqldb}, nil
|
|
}
|
|
|
|
// Close closes the underlying database.
|
|
func (d *DB) Close() error { return d.sql.Close() }
|
|
|
|
// SQL exposes the underlying *sql.DB for hosts that need direct access.
|
|
func (d *DB) SQL() *sql.DB { return d.sql }
|