mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 11:31:57 +00:00
Tools: list_tables, describe_table, select_table, insert_into_table, update_table, delete_from_table, list_functions, call_function. Visibility follows the model rules. Filter-based update/delete require filters (never dropped silently), cap the matched rows (MaxWriteRows), support dry_run, and need a single-use confirm token bound to caller, table, filters, data and the matched rows. RegisterFunction adds Go-callback and SQL-procedure functions run in a transaction with BeforeCall/AfterCall hooks. Per-model tools and resources are removed. fix(pgsql): UPDATE with SET and a multi-placeholder WHERE renumbered the WHERE parameters wrongly ($1, $2 became $3, $2); shift them in one pass.
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package resolvemcp
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// confirmStore holds the single-use confirmation tokens for filter-based writes. It is
|
|
// in-memory: tokens are lost on restart and are not shared between instances, which only costs
|
|
// the client one more preview call.
|
|
type confirmStore struct {
|
|
mu sync.Mutex
|
|
tokens map[string]confirmEntry
|
|
now func() time.Time
|
|
maxLive int
|
|
}
|
|
|
|
type confirmEntry struct {
|
|
user, table, op, binding string
|
|
expires time.Time
|
|
}
|
|
|
|
func newConfirmStore() *confirmStore {
|
|
return &confirmStore{tokens: map[string]confirmEntry{}, now: time.Now, maxLive: 10000}
|
|
}
|
|
|
|
var errConfirmInvalid = NewClientError(CodeInvalidArgument, "confirm_token is invalid or expired; repeat the call without it to get a new preview")
|
|
|
|
// issue returns a token bound to the caller, table, operation and binding (a hash of the
|
|
// filters, data and matched rows the preview showed).
|
|
func (c *confirmStore) issue(user, table, op, binding string, ttl time.Duration) (string, error) {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return "", err
|
|
}
|
|
tok := hex.EncodeToString(b[:])
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
now := c.now()
|
|
for k, e := range c.tokens {
|
|
if now.After(e.expires) {
|
|
delete(c.tokens, k)
|
|
}
|
|
}
|
|
if len(c.tokens) >= c.maxLive {
|
|
return "", NewClientError(CodeLimitExceeded, "too many pending confirmations; try again later")
|
|
}
|
|
c.tokens[tok] = confirmEntry{user: user, table: table, op: op, binding: binding, expires: now.Add(ttl)}
|
|
return tok, nil
|
|
}
|
|
|
|
// consume validates and removes a token. Any mismatch (other user, table, operation or
|
|
// changed binding) is the same error, and the token is spent either way.
|
|
func (c *confirmStore) consume(tok, user, table, op, binding string) error {
|
|
c.mu.Lock()
|
|
e, ok := c.tokens[tok]
|
|
delete(c.tokens, tok)
|
|
now := c.now()
|
|
c.mu.Unlock()
|
|
if !ok || now.After(e.expires) || e.user != user || e.table != table || e.op != op || e.binding != binding {
|
|
return errConfirmInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bindingHash fingerprints the parts of a write a confirmation covers.
|
|
func bindingHash(parts ...any) (string, error) {
|
|
h := sha256.New()
|
|
for _, p := range parts {
|
|
b, err := json.Marshal(p)
|
|
if err != nil {
|
|
return "", errors.New("cannot fingerprint request")
|
|
}
|
|
h.Write(b)
|
|
h.Write([]byte{0})
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|