Harden durable prompt state contracts
This commit is contained in:
@@ -67,21 +67,30 @@ func (a PromptPreparationArtifact) Validate() error {
|
||||
if a.SchemaVersion != PromptPreparationSchemaVersion {
|
||||
return fmt.Errorf("unsupported prompt preparation schema version %q", a.SchemaVersion)
|
||||
}
|
||||
if strings.TrimSpace(a.RunID) == "" || a.ReportID == "" || strings.TrimSpace(a.PromptID) == "" {
|
||||
return fmt.Errorf("prompt preparation identity is required")
|
||||
if err := validatePromptArtifactIdentity("prompt preparation", a.ReportID, a.RunID, a.PromptID, a.PromptVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(a.DataPackagePath) == "" {
|
||||
return fmt.Errorf("prompt preparation data package path is required")
|
||||
}
|
||||
if err := validatePromptArtifactTiming("prompt preparation", a.StartedAt, a.EndedAt, a.Duration); err != nil {
|
||||
return err
|
||||
}
|
||||
switch a.Status {
|
||||
case PromptPreparationSucceeded:
|
||||
if a.Preparation == nil || a.Error != nil {
|
||||
return fmt.Errorf("successful prompt preparation requires preparation without an error")
|
||||
}
|
||||
if a.Preparation.PromptID != a.PromptID || a.Preparation.PromptVersion != a.PromptVersion || a.Preparation.DataPackagePath != a.DataPackagePath {
|
||||
return fmt.Errorf("successful prompt preparation provenance must match the artifact")
|
||||
}
|
||||
case PromptPreparationFailed:
|
||||
if !validPromptArtifactError(a.Error) {
|
||||
return fmt.Errorf("failed prompt preparation requires a classified error")
|
||||
}
|
||||
if a.Preparation != nil {
|
||||
return fmt.Errorf("failed prompt preparation must not include preparation provenance")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported prompt preparation status %q", a.Status)
|
||||
}
|
||||
@@ -164,22 +173,34 @@ func (a PromptExecutionArtifact) Validate() error {
|
||||
if a.SchemaVersion != PromptExecutionSchemaVersion {
|
||||
return fmt.Errorf("unsupported prompt execution schema version %q", a.SchemaVersion)
|
||||
}
|
||||
if strings.TrimSpace(a.RunID) == "" || a.ReportID == "" || strings.TrimSpace(a.PromptID) == "" {
|
||||
return fmt.Errorf("prompt execution identity is required")
|
||||
if err := validatePromptArtifactIdentity("prompt execution", a.ReportID, a.RunID, a.PromptID, a.PromptVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validatePromptArtifactTiming("prompt execution", a.StartedAt, a.EndedAt, a.Duration); err != nil {
|
||||
return err
|
||||
}
|
||||
switch a.Status {
|
||||
case PromptExecutionSucceeded:
|
||||
if a.Provenance == nil || a.Validation == nil || a.Validation.Status != promptexec.ValidationPassed || a.Error != nil {
|
||||
return fmt.Errorf("successful prompt execution requires passed validation without an error")
|
||||
}
|
||||
if err := validatePromptExecutionProvenance(a); err != nil {
|
||||
return err
|
||||
}
|
||||
case PromptExecutionValidationRejected:
|
||||
if a.Provenance == nil || a.Validation == nil || a.Validation.Status != promptexec.ValidationFailed || a.Error != nil {
|
||||
return fmt.Errorf("validation-rejected prompt execution requires failed validation without an error")
|
||||
}
|
||||
if err := validatePromptExecutionProvenance(a); err != nil {
|
||||
return err
|
||||
}
|
||||
case PromptExecutionFailed:
|
||||
if !validPromptArtifactError(a.Error) {
|
||||
return fmt.Errorf("failed prompt execution requires a classified error")
|
||||
}
|
||||
if a.Provenance != nil || a.Validation != nil {
|
||||
return fmt.Errorf("failed prompt execution must not include completed provenance or validation")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported prompt execution status %q", a.Status)
|
||||
}
|
||||
@@ -187,5 +208,74 @@ func (a PromptExecutionArtifact) Validate() error {
|
||||
}
|
||||
|
||||
func validPromptArtifactError(value *PromptArtifactError) bool {
|
||||
return value != nil && value.Category != "" && strings.TrimSpace(value.Message) != "" && len(value.Message) <= promptArtifactErrorLimit && utf8.ValidString(value.Message)
|
||||
return value != nil && validPromptErrorCategory(value.Category) && strings.TrimSpace(value.Message) != "" && len(value.Message) <= promptArtifactErrorLimit && utf8.ValidString(value.Message)
|
||||
}
|
||||
|
||||
func validatePromptArtifactIdentity(kind string, reportID report.ID, runID, promptID, promptVersion string) error {
|
||||
if reportID == "" || strings.TrimSpace(runID) == "" || strings.TrimSpace(promptID) == "" {
|
||||
return fmt.Errorf("%s identity is required", kind)
|
||||
}
|
||||
definition, err := report.DefaultRegistry().Lookup(reportID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s report id is unsupported: %w", kind, err)
|
||||
}
|
||||
if promptID != definition.PromptID {
|
||||
return fmt.Errorf("%s prompt id must match report %q", kind, reportID)
|
||||
}
|
||||
if promptVersion != definition.PromptVersion {
|
||||
return fmt.Errorf("%s prompt version must be %q", kind, definition.PromptVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePromptArtifactTiming(kind string, startedAt, endedAt time.Time, duration time.Duration) error {
|
||||
if startedAt.IsZero() || endedAt.IsZero() {
|
||||
return fmt.Errorf("%s start and end times are required", kind)
|
||||
}
|
||||
if duration < 0 {
|
||||
return fmt.Errorf("%s duration must not be negative", kind)
|
||||
}
|
||||
if endedAt.Before(startedAt) {
|
||||
return fmt.Errorf("%s end time must not be earlier than its start time", kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePromptExecutionProvenance(artifact PromptExecutionArtifact) error {
|
||||
value := artifact.Provenance
|
||||
if value == nil {
|
||||
return fmt.Errorf("completed prompt execution provenance is required")
|
||||
}
|
||||
if value.PromptID != artifact.PromptID || value.PromptVersion != artifact.PromptVersion {
|
||||
return fmt.Errorf("completed prompt execution provenance must match the artifact")
|
||||
}
|
||||
for _, required := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"run id", value.RunID}, {"prompt hash", value.PromptHash}, {"rendered prompt hash", value.RenderedPromptHash},
|
||||
{"profile id", value.ProfileID}, {"backend id", value.BackendID}, {"model name", value.ModelName},
|
||||
{"data package path", value.DataPackagePath},
|
||||
} {
|
||||
if strings.TrimSpace(required.value) == "" {
|
||||
return fmt.Errorf("completed prompt execution provenance %s is required", required.name)
|
||||
}
|
||||
}
|
||||
if err := validatePromptArtifactTiming("completed prompt execution provenance", value.StartedAt, value.EndedAt, value.Duration); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validPromptErrorCategory(category promptexec.ErrorCategory) bool {
|
||||
switch category {
|
||||
case promptexec.InvalidConfiguration, promptexec.InvalidRequest, promptexec.PromptNotFound,
|
||||
promptexec.PromptLoad, promptexec.ProfileNotFound, promptexec.ProfileLoad,
|
||||
promptexec.MissingCredential, promptexec.ArtifactLoad, promptexec.PromptRender,
|
||||
promptexec.Capacity, promptexec.Generation, promptexec.OperationalValidation,
|
||||
promptexec.ValidationRejected, promptexec.Canceled, promptexec.DeadlineExceeded:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user