173 lines
5.8 KiB
Go
173 lines
5.8 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/ai"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
"git.ipao.vip/rogee/go-sip/internal/mq"
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
)
|
|
|
|
var ErrAIConfigResponse = errors.New("invalid AI configuration response")
|
|
|
|
func (s *Store) QueueAIConfigRequest(raw []byte) error {
|
|
request, err := contract.DecodeService(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if request.MessageType != "ai.config.request" {
|
|
return errors.New("expected AI configuration request")
|
|
}
|
|
route, err := tenant.NewDispatcherRoute(request.DispatcherID, request.TenantKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var payload struct {
|
|
AgentVersionID string `json:"agent_version_id"`
|
|
}
|
|
if err := json.Unmarshal(request.Payload, &payload); err != nil {
|
|
return err
|
|
}
|
|
sum := sha256.Sum256(raw)
|
|
digest := hex.EncodeToString(sum[:])
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if err := bindMQScope(tx, request.DispatcherID, request.TenantID, request.TenantKey); err != nil {
|
|
return err
|
|
}
|
|
var previous string
|
|
err = tx.QueryRow(`SELECT body_hash FROM ai_mq_requests WHERE tenant_id=? AND message_id=?`, request.TenantID, request.MessageID).Scan(&previous)
|
|
if err == nil {
|
|
if previous != digest {
|
|
return ErrIdempotencyConflict
|
|
}
|
|
return nil
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
deadline, err := time.Parse(time.RFC3339Nano, request.NotAfter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !s.now().Before(deadline) {
|
|
return errors.New("AI configuration request expired")
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO outbox(event_id,tenant_key,exchange,routing_key,body,status,created_at) VALUES(?,?,?,?,?,'pending',?)`, request.MessageID, request.TenantKey, mq.EventExchange, route.OutboundKey, raw, s.now().UTC().Format(time.RFC3339Nano)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO ai_mq_requests(tenant_id,message_id,tenant_key,dispatcher_id,agent_version_id,body,body_hash) VALUES(?,?,?,?,?,?,?)`, request.TenantID, request.MessageID, request.TenantKey, request.DispatcherID, payload.AgentVersionID, raw, digest); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// StoreAIConfigResponse records the original response and immutable snapshot
|
|
// before the consumer ACK. Configuration alone is not execution authorization.
|
|
func (s *Store) StoreAIConfigResponse(raw []byte, routingKey string) error {
|
|
response, err := contract.DecodeService(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if response.MessageType != "ai.config.result" {
|
|
return ErrAIConfigResponse
|
|
}
|
|
route, err := tenant.NewDispatcherRoute(response.DispatcherID, response.TenantKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if routingKey != route.InboundKey {
|
|
return ErrMessageScope
|
|
}
|
|
sum := sha256.Sum256(raw)
|
|
digest := hex.EncodeToString(sum[:])
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if err := bindMQScope(tx, response.DispatcherID, response.TenantID, response.TenantKey); err != nil {
|
|
return err
|
|
}
|
|
var requestRaw []byte
|
|
var previousID, previousHash sql.NullString
|
|
err = tx.QueryRow(`SELECT body,response_id,response_hash FROM ai_mq_requests WHERE tenant_id=? AND tenant_key=? AND message_id=?`, response.TenantID, response.TenantKey, response.CorrelationID).Scan(&requestRaw, &previousID, &previousHash)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ErrMessageScope
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if previousID.Valid {
|
|
if previousID.String != response.MessageID || previousHash.String != digest {
|
|
return ErrIdempotencyConflict
|
|
}
|
|
return nil
|
|
}
|
|
request, err := contract.DecodeService(requestRaw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if response.Status == "ok" {
|
|
snapshot, err := ai.ValidateConfigResponse(raw, request, s.now())
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrAIConfigResponse, err)
|
|
}
|
|
var payload struct {
|
|
Authorization json.RawMessage `json:"authorization"`
|
|
}
|
|
if err := json.Unmarshal(response.Payload, &payload); err != nil {
|
|
return err
|
|
}
|
|
if _, err := ai.DecodeBoundAuthorization(payload.Authorization, snapshot, response.TenantID, response.TenantKey); err != nil {
|
|
return fmt.Errorf("%w: %v", ErrAIConfigResponse, err)
|
|
}
|
|
var existing string
|
|
err = tx.QueryRow(`SELECT config_sha256 FROM ai_config_snapshots WHERE tenant_id=? AND agent_version_id=?`, response.TenantID, snapshot.AgentVersionID).Scan(&existing)
|
|
if err == nil && existing != snapshot.Digest {
|
|
return ErrIdempotencyConflict
|
|
}
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO ai_config_snapshots(tenant_id,agent_version_id,tenant_key,config_sha256,config) VALUES(?,?,?,?,?) ON CONFLICT DO NOTHING`, response.TenantID, snapshot.AgentVersionID, response.TenantKey, snapshot.Digest, snapshot.Raw); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := tx.Exec(`UPDATE ai_mq_requests SET response_id=?,response_hash=?,response=? WHERE tenant_id=? AND message_id=?`, response.MessageID, digest, raw, response.TenantID, response.CorrelationID); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *Store) LoadAIConfig(tenantID, tenantKey, version string) (ai.Snapshot, error) {
|
|
var raw []byte
|
|
var digest string
|
|
if err := s.db.QueryRow(`SELECT config,config_sha256 FROM ai_config_snapshots WHERE tenant_id=? AND tenant_key=? AND agent_version_id=?`, tenantID, tenantKey, version).Scan(&raw, &digest); err != nil {
|
|
return ai.Snapshot{}, err
|
|
}
|
|
snapshot, err := ai.Validate(raw)
|
|
if err != nil {
|
|
return ai.Snapshot{}, err
|
|
}
|
|
if snapshot.Digest != digest || snapshot.AgentVersionID != version {
|
|
return ai.Snapshot{}, errors.New("persisted AI configuration integrity mismatch")
|
|
}
|
|
snapshot.TenantKey = tenantKey
|
|
return snapshot, nil
|
|
}
|