Files
go-sip/internal/store/ai_mq.go
T

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
}