75 lines
2.9 KiB
Go
75 lines
2.9 KiB
Go
package ai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
// ValidateConfigResponse binds an incoming configuration to the original,
|
|
// persisted request. It does not infer authorization from a published version.
|
|
func ValidateConfigResponse(raw []byte, request contract.ServiceMessage, now time.Time) (Snapshot, error) {
|
|
if request.MessageType != "ai.config.request" {
|
|
return Snapshot{}, errors.New("expected original AI configuration request")
|
|
}
|
|
response, err := contract.DecodeService(raw)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
if response.MessageType != "ai.config.result" || response.CorrelationID != request.MessageID || response.DispatcherID != request.DispatcherID || response.TenantID != request.TenantID || response.TenantKey != request.TenantKey {
|
|
return Snapshot{}, errors.New("AI configuration response binding mismatch")
|
|
}
|
|
deadline, err := time.Parse(time.RFC3339Nano, request.NotAfter)
|
|
if err != nil {
|
|
return Snapshot{}, fmt.Errorf("AI request deadline: %w", err)
|
|
}
|
|
issued, err := time.Parse(time.RFC3339Nano, request.IssuedAt)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
responseTime, err := time.Parse(time.RFC3339Nano, response.IssuedAt)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
if !now.Before(deadline) || now.Before(issued) || responseTime.Before(issued) || responseTime.After(now) {
|
|
return Snapshot{}, errors.New("AI configuration response is outside the request window")
|
|
}
|
|
if response.Status != "ok" {
|
|
return Snapshot{}, fmt.Errorf("AI configuration request rejected: %s", response.ReasonCode)
|
|
}
|
|
var requested struct {
|
|
AgentVersionID string `json:"agent_version_id"`
|
|
}
|
|
if err := json.Unmarshal(request.Payload, &requested); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
var payload struct {
|
|
Snapshot struct {
|
|
AgentVersionID string `json:"agent_version_id"`
|
|
Status string `json:"status"`
|
|
TenantID string `json:"tenant_id"`
|
|
Immutable bool `json:"immutable"`
|
|
ContentSHA256 string `json:"content_sha256"`
|
|
Config json.RawMessage `json:"config"`
|
|
} `json:"snapshot"`
|
|
}
|
|
if err := json.Unmarshal(response.Payload, &payload); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
if (payload.Snapshot.Status != "published" && payload.Snapshot.Status != "reused") || payload.Snapshot.AgentVersionID != requested.AgentVersionID || payload.Snapshot.TenantID != request.TenantID || !payload.Snapshot.Immutable {
|
|
return Snapshot{}, errors.New("AI snapshot publication, version, tenant or immutability mismatch")
|
|
}
|
|
snapshot, err := Validate(payload.Snapshot.Config)
|
|
if err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
if snapshot.AgentVersionID != requested.AgentVersionID || snapshot.Digest != payload.Snapshot.ContentSHA256 {
|
|
return Snapshot{}, errors.New("AI configuration version or canonical digest mismatch")
|
|
}
|
|
snapshot.TenantKey = request.TenantKey
|
|
return snapshot, nil
|
|
}
|