package iop import ( "context" "errors" "fmt" "strings" "time" "git.toki-labs.com/toki/ariadne/internal/domain/errorrecord" "git.toki-labs.com/toki/ariadne/internal/domain/failure" "git.toki-labs.com/toki/ariadne/internal/domain/organization" "git.toki-labs.com/toki/ariadne/internal/domain/remediation" "github.com/google/uuid" ) // Executor is the Ariadne-owned boundary for LLM and CLI work. Implementations // may use IOP's native protocol without leaking that protocol into the domain. type Executor interface { Start(context.Context, Request) (Execution, error) Events( context.Context, organization.ID, string, uint64, ) (EventStream, error) Cancel(context.Context, organization.ID, string) error } type EventStream interface { Receive(context.Context) (Event, error) Close() error } type Intent string const ( IntentAnalyze Intent = "analyze" IntentApplyChange Intent = "apply_change" ) type RepositoryAccess string const ( RepositoryAccessReadOnly RepositoryAccess = "read_only" RepositoryAccessIsolatedBranch RepositoryAccess = "isolated_branch" ) const RuntimeAccessReadOnly = "read_only" type Request struct { ExecutionID string OrganizationID organization.ID ProjectID string IngestionSourceID string ErrorGroupID string Intent Intent Mode remediation.Mode ProposalDigest string Error errorrecord.Event Repository RepositoryTarget RuntimeTargets []RuntimeTarget Policy Policy Deadline time.Time } type RepositoryTarget struct { RepositoryID string WorkspaceReference string BaseRevision string } type RuntimeTarget struct { EnvironmentID string ConnectionReference string } type Policy struct { RepositoryAccess RepositoryAccess RuntimeAccess string AllowMerge bool } type Execution struct { ID string StartedAt time.Time } type EventType string const ( EventStarted EventType = "started" EventProgress EventType = "progress" EventAnalysisResult EventType = "analysis_result" EventChangeResult EventType = "change_result" EventValidationResult EventType = "validation_result" EventCompleted EventType = "completed" EventFailed EventType = "failed" EventCancelled EventType = "cancelled" ) type Event struct { ExecutionID string Sequence uint64 Type EventType Message string Analysis *AnalysisResult Change *ChangeResult Validation *ValidationResult Failure *failure.Failure OccurredAt time.Time } type AnalysisResult struct { Fixability remediation.Fixability Report string ProposalDigest string } type ChangeResult struct { Changed bool Branch string PatchReference string } type ValidationResult struct { Passed bool Summary string ArtifactReferences []string } func (request Request) Validate() error { var validationErrors []error if _, err := uuid.Parse(request.ExecutionID); err != nil { validationErrors = append(validationErrors, fmt.Errorf("execution id: %w", err)) } if err := request.OrganizationID.Validate(); err != nil { validationErrors = append(validationErrors, fmt.Errorf("organization id: %w", err)) } for field, value := range map[string]string{ "project id": request.ProjectID, "ingestion source id": request.IngestionSourceID, "error group id": request.ErrorGroupID, "repository id": request.Repository.RepositoryID, } { if _, err := uuid.Parse(value); err != nil { validationErrors = append(validationErrors, fmt.Errorf("%s: %w", field, err)) } } if request.Intent != IntentAnalyze && request.Intent != IntentApplyChange { validationErrors = append(validationErrors, errors.New("execution intent is invalid")) } if _, err := remediation.ParseMode(string(request.Mode)); err != nil { validationErrors = append(validationErrors, err) } if err := request.Error.Validate(); err != nil { validationErrors = append(validationErrors, fmt.Errorf("error event: %w", err)) } if request.Error.OrganizationID != request.OrganizationID || request.Error.ProjectID != request.ProjectID || request.Error.IngestionSourceID != request.IngestionSourceID { validationErrors = append( validationErrors, errors.New("error event scope does not match execution scope"), ) } if strings.TrimSpace(request.Repository.WorkspaceReference) == "" { validationErrors = append( validationErrors, errors.New("repository workspace reference is required"), ) } if strings.TrimSpace(request.Repository.BaseRevision) == "" { validationErrors = append( validationErrors, errors.New("repository base revision is required"), ) } for index, target := range request.RuntimeTargets { if _, err := uuid.Parse(target.EnvironmentID); err != nil { validationErrors = append( validationErrors, fmt.Errorf("runtime target %d environment id: %w", index, err), ) } if strings.TrimSpace(target.ConnectionReference) == "" { validationErrors = append( validationErrors, fmt.Errorf("runtime target %d connection reference is required", index), ) } } if request.Policy.RuntimeAccess != RuntimeAccessReadOnly { validationErrors = append( validationErrors, errors.New("runtime access must be read-only"), ) } if request.Policy.AllowMerge { validationErrors = append( validationErrors, errors.New("IOP must not receive merge permission"), ) } switch request.Policy.RepositoryAccess { case RepositoryAccessReadOnly, RepositoryAccessIsolatedBranch: default: validationErrors = append( validationErrors, errors.New("repository access policy is invalid"), ) } if request.Intent == IntentAnalyze && request.Policy.RepositoryAccess != RepositoryAccessReadOnly { validationErrors = append( validationErrors, errors.New("analysis execution must use read-only repository access"), ) } if request.Intent == IntentApplyChange && request.Policy.RepositoryAccess != RepositoryAccessIsolatedBranch { validationErrors = append( validationErrors, errors.New("change execution must use an isolated branch"), ) } switch request.Intent { case IntentAnalyze: if strings.TrimSpace(request.ProposalDigest) != "" { validationErrors = append( validationErrors, errors.New("analysis execution must not include a proposal digest"), ) } case IntentApplyChange: if strings.TrimSpace(request.ProposalDigest) == "" { validationErrors = append( validationErrors, errors.New("change execution requires the authorized proposal digest"), ) } } if request.Deadline.IsZero() { validationErrors = append(validationErrors, errors.New("execution deadline is required")) } return errors.Join(validationErrors...) } func (event Event) Validate() error { var validationErrors []error if _, err := uuid.Parse(event.ExecutionID); err != nil { validationErrors = append(validationErrors, fmt.Errorf("execution id: %w", err)) } if event.OccurredAt.IsZero() { validationErrors = append(validationErrors, errors.New("event occurrence time is required")) } switch event.Type { case EventStarted, EventProgress, EventCompleted, EventCancelled: case EventAnalysisResult: if event.Analysis == nil { validationErrors = append(validationErrors, errors.New("analysis result is required")) break } if _, err := remediation.ParseFixability(string(event.Analysis.Fixability)); err != nil { validationErrors = append(validationErrors, err) } if strings.TrimSpace(event.Analysis.Report) == "" { validationErrors = append(validationErrors, errors.New("analysis report is required")) } if event.Analysis.Fixability == remediation.FixabilityFixable && strings.TrimSpace(event.Analysis.ProposalDigest) == "" { validationErrors = append( validationErrors, errors.New("fixable analysis requires a proposal digest"), ) } if event.Analysis.Fixability != remediation.FixabilityFixable && strings.TrimSpace(event.Analysis.ProposalDigest) != "" { validationErrors = append( validationErrors, errors.New("only a fixable analysis may include a proposal digest"), ) } case EventChangeResult: if event.Change == nil { validationErrors = append(validationErrors, errors.New("change result is required")) break } if event.Change.Changed && (strings.TrimSpace(event.Change.Branch) == "" || strings.TrimSpace(event.Change.PatchReference) == "") { validationErrors = append( validationErrors, errors.New("a changed result requires branch and patch references"), ) } if !event.Change.Changed && (strings.TrimSpace(event.Change.Branch) != "" || strings.TrimSpace(event.Change.PatchReference) != "") { validationErrors = append( validationErrors, errors.New("an unchanged result must not include branch or patch references"), ) } case EventValidationResult: if event.Validation == nil { validationErrors = append(validationErrors, errors.New("validation result is required")) } else if strings.TrimSpace(event.Validation.Summary) == "" { validationErrors = append(validationErrors, errors.New("validation summary is required")) } case EventFailed: if event.Failure == nil { validationErrors = append(validationErrors, errors.New("failure is required")) } else if err := event.Failure.Validate(); err != nil { validationErrors = append(validationErrors, fmt.Errorf("failure: %w", err)) } default: validationErrors = append(validationErrors, errors.New("execution event type is invalid")) } payloadCount := 0 for _, present := range []bool{ event.Analysis != nil, event.Change != nil, event.Validation != nil, event.Failure != nil, } { if present { payloadCount++ } } expectedPayloadCount := 0 switch event.Type { case EventAnalysisResult, EventChangeResult, EventValidationResult, EventFailed: expectedPayloadCount = 1 } if payloadCount != expectedPayloadCount { validationErrors = append( validationErrors, fmt.Errorf( "execution event payload count = %d, want %d for %q", payloadCount, expectedPayloadCount, event.Type, ), ) } return errors.Join(validationErrors...) }