package config import ( "fmt" "path/filepath" "sort" "strings" "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/notariusref" ) func validateNotarius(cfg *NotariusConfig, scriptorium *ScriptoriumConfig) error { if cfg == nil || !cfg.Enabled { return nil } binary := strings.TrimSpace(cfg.Binary) configPath := filepath.Clean(cfg.ConfigPath) pipelineID := strings.TrimSpace(cfg.PipelineID) timeout := strings.TrimSpace(cfg.Timeout) workingDirectory := filepath.Clean(cfg.WorkingDirectory) if binary == "" { return fmt.Errorf("pipeline.notarius.binary is required when pipeline.notarius.enabled is true") } if strings.TrimSpace(cfg.ConfigPath) == "" { return fmt.Errorf("pipeline.notarius.config_path is required when pipeline.notarius.enabled is true") } if pipelineID == "" { return fmt.Errorf("pipeline.notarius.pipeline_id is required when pipeline.notarius.enabled is true") } if timeout == "" { return fmt.Errorf("pipeline.notarius.timeout must be a valid duration") } if err := validateDuration("pipeline.notarius.timeout", timeout); err != nil { return err } if strings.TrimSpace(cfg.WorkingDirectory) == "" { return fmt.Errorf("pipeline.notarius.working_directory is required when pipeline.notarius.enabled is true") } if len(cfg.Outputs) == 0 { return fmt.Errorf("pipeline.notarius.outputs must contain at least one output when pipeline.notarius.enabled is true") } if len(cfg.References) > MaxNotariusReferenceBindings { return fmt.Errorf("pipeline.notarius.references must contain at most %d bindings", MaxNotariusReferenceBindings) } references, err := normalizeNotariusReferences(cfg.References) if err != nil { return err } outputs, err := normalizeNotariusOutputs(cfg.Outputs, scriptorium) if err != nil { return err } cfg.Binary = binary cfg.ConfigPath = configPath cfg.PipelineID = pipelineID cfg.Timeout = timeout cfg.WorkingDirectory = workingDirectory cfg.References = references cfg.Outputs = outputs return nil } func normalizeNotariusReferences(references map[string]string) (map[string]string, error) { selectors := make([]string, 0, len(references)) for selector := range references { selectors = append(selectors, selector) } sort.Strings(selectors) var normalized map[string]string if references != nil { normalized = make(map[string]string, len(references)) } owners := make(map[string]string, len(references)) for _, rawSelector := range selectors { selector, err := notariusref.NormalizeSelector(rawSelector) if err != nil { return nil, fmt.Errorf("pipeline.notarius.references selector %q is invalid: %w", rawSelector, err) } if previous, ok := owners[selector]; ok { return nil, fmt.Errorf("pipeline.notarius.references selectors %q and %q normalize to %q", previous, rawSelector, selector) } owners[selector] = rawSelector source := strings.TrimSpace(references[rawSelector]) if source == "" { return nil, fmt.Errorf("pipeline.notarius.references.%s source is required", selector) } if _, ok := artifactpolicy.DescribePreparedInputSource(source); !ok { return nil, fmt.Errorf("pipeline.notarius.references.%s source %q is not a supported prepared input source", selector, source) } normalized[selector] = source } return normalized, nil } func normalizeNotariusOutputs(outputs map[string]NotariusOutputConfig, scriptorium *ScriptoriumConfig) (map[string]NotariusOutputConfig, error) { reservedSources := notariusReservedSources(scriptorium) keys := make([]string, 0, len(outputs)) for key := range outputs { keys = append(keys, key) } sort.Strings(keys) normalized := make(map[string]NotariusOutputConfig, len(outputs)) sourceOwners := make(map[string]string, len(outputs)) laneOwners := make(map[string]string, len(outputs)) for _, rawKey := range keys { output := outputs[rawKey] key := strings.TrimSpace(rawKey) if !artifactpolicy.IsConfiguredKey(key) { return nil, fmt.Errorf("pipeline.notarius.outputs keys must match ^[a-z][a-z0-9_]*$") } sourceID := artifactpolicy.ExtractionSourceID(key) if previousKey, ok := sourceOwners[sourceID]; ok { return nil, fmt.Errorf("pipeline.notarius.outputs keys %q and %q normalize to duplicate source %q", previousKey, rawKey, sourceID) } if owner, ok := reservedSources[sourceID]; ok { return nil, fmt.Errorf("pipeline.notarius.outputs.%s source %q collides with %s", key, sourceID, owner) } sourceOwners[sourceID] = rawKey output.LaneID = strings.TrimSpace(output.LaneID) output.MediaType = strings.TrimSpace(output.MediaType) output.SchemaID = strings.TrimSpace(output.SchemaID) output.SchemaVersion = strings.TrimSpace(output.SchemaVersion) output.ModuleKey = strings.TrimSpace(output.ModuleKey) prefix := "pipeline.notarius.outputs." + key if output.LaneID == "" { return nil, fmt.Errorf("%s.lane_id is required", prefix) } if previousKey, ok := laneOwners[output.LaneID]; ok { return nil, fmt.Errorf("%s.lane_id %q duplicates pipeline.notarius.outputs.%s.lane_id", prefix, output.LaneID, previousKey) } laneOwners[output.LaneID] = key if output.MediaType == "" { return nil, fmt.Errorf("%s.media_type is required", prefix) } if output.SchemaID == "" { return nil, fmt.Errorf("%s.schema_id is required", prefix) } if output.SchemaVersion == "" { return nil, fmt.Errorf("%s.schema_version is required", prefix) } normalized[key] = output } return normalized, nil } func notariusReservedSources(scriptorium *ScriptoriumConfig) map[string]string { reserved := make(map[string]string) for _, spec := range artifactmodel.RuntimeTranscriptArtifacts() { reserved[spec.SourceID] = "built-in source" } reserved[artifactpolicy.SourceBoundsSession] = "built-in source" if scriptorium != nil { for key := range scriptorium.Artifacts { normalizedKey := strings.TrimSpace(key) reserved[artifactpolicy.ConfiguredSourceID(normalizedKey)] = "configured Scriptorium source" reserved[artifactpolicy.PreviousSessionSourceID(normalizedKey)] = "previous-session source" } } return reserved } func notariusOutputKeySet(notarius *NotariusConfig) map[string]struct{} { out := map[string]struct{}{} if notarius == nil { return out } for key := range notarius.Outputs { trimmed := strings.TrimSpace(key) if artifactpolicy.IsConfiguredKey(trimmed) { out[trimmed] = struct{}{} } } return out }