package taskloop import ( "context" "errors" "fmt" "os" "path/filepath" "reflect" "strings" "iop/packages/go/agentconfig" "iop/packages/go/agentguard" "iop/packages/go/agenttask" "iop/packages/go/agentworkspace" ) // ReviewExecutor invokes the official review provider boundary. Tests inject a // deterministic fake; production uses the common catalog provider. type ReviewExecutor interface { ExecuteReview(context.Context, agenttask.ReviewRequest, string, string, string) error } // ArtifactRootResolver resolves only the retained task view associated with an // exact work attempt. Evidence and recovery never fall back to the canonical // registered workspace because provider output exists only in the task view. type ArtifactRootResolver interface { ArtifactRoot(agenttask.WorkRecord) (string, error) } type retainedArtifactResolver struct { backend *agentworkspace.Backend } type retainedIsolationBackend interface { agenttask.IsolationBackend LoadRecord(agentguard.IsolationDescriptor) (agentworkspace.OverlayRecord, error) } func (resolver retainedArtifactResolver) ArtifactRoot( work agenttask.WorkRecord, ) (string, error) { descriptor, err := loadOverlayDescriptor(resolver.backend, work) if err != nil { return "", err } return descriptor.WorkingDir, nil } type Reviewer struct { snapshot agentconfig.RuntimeSnapshot backend *agentworkspace.Backend executor ReviewExecutor } var _ agenttask.Reviewer = (*Reviewer)(nil) func NewReviewer( snapshot agentconfig.RuntimeSnapshot, backend *agentworkspace.Backend, executor ReviewExecutor, ) (*Reviewer, error) { if backend == nil { return nil, errors.New("taskloop: review requires the retained workspace backend") } return &Reviewer{snapshot: snapshot, backend: backend, executor: executor}, nil } func (reviewer *Reviewer) Review( ctx context.Context, request agenttask.ReviewRequest, ) (agenttask.ReviewResult, error) { if err := ctx.Err(); err != nil { return agenttask.ReviewResult{}, err } if request.Work.Submission == nil || request.Work.Submission.ArtifactID != request.Submission.ArtifactID || request.Work.AttemptID != request.Submission.AttemptID { return agenttask.ReviewResult{}, errors.New("taskloop: review submission identity mismatch") } _, ok := reviewer.snapshot.Project(string(request.Project.ProjectID)) if !ok { return agenttask.ReviewResult{}, errors.New("taskloop: review project is not registered") } planRelative := request.Work.Unit.Metadata["plan_path"] reviewRelative := request.Work.Unit.Metadata["review_path"] if planRelative == "" || reviewRelative == "" { return agenttask.ReviewResult{}, errors.New("taskloop: review artifact locators are incomplete") } root, err := reviewer.taskView(request.Work) if err != nil { return agenttask.ReviewResult{}, err } reviewPath := filepath.Join(root, filepath.FromSlash(reviewRelative)) verdict, message, verdictErr := readReviewVerdict(reviewPath) if errors.Is(verdictErr, errVerdictMissing) && reviewer.executor != nil { if err := reviewer.executor.ExecuteReview( ctx, request, root, planRelative, reviewRelative, ); err != nil { return agenttask.ReviewResult{}, fmt.Errorf("taskloop: official review invocation: %w", err) } verdict, message, verdictErr = readReviewVerdict(reviewPath) } if verdictErr != nil { return agenttask.ReviewResult{}, verdictErr } result := agenttask.ReviewResult{ ProjectID: request.Project.ProjectID, WorkUnitID: request.Work.Unit.ID, AttemptID: request.Work.AttemptID, ArtifactID: request.Submission.ArtifactID, Verdict: verdict, Message: message, Rework: verdict == agenttask.ReviewVerdictWarn || verdict == agenttask.ReviewVerdictFail, } if verdict != agenttask.ReviewVerdictPass { return result, nil } descriptor, err := reviewer.overlayDescriptor(request.Work) if err != nil { return agenttask.ReviewResult{}, err } content, err := os.ReadFile(reviewPath) if err != nil { return agenttask.ReviewResult{}, err } changeSet, err := reviewer.backend.Freeze(ctx, agentworkspace.FreezeRequest{ Descriptor: descriptor, ArtifactID: request.Submission.ArtifactID, ValidationEvidence: []agentworkspace.ValidationEvidence{{ Name: "official-review", Result: "pass", Digest: digestBytes(reviewRelative, content), }}, }) if err != nil { return agenttask.ReviewResult{}, fmt.Errorf("taskloop: freeze reviewed change set: %w", err) } identity := changeSet.Identity() result.ChangeSet = &identity return result, nil } func (reviewer *Reviewer) taskView(work agenttask.WorkRecord) (string, error) { return retainedArtifactResolver{backend: reviewer.backend}.ArtifactRoot(work) } func (reviewer *Reviewer) overlayDescriptor( work agenttask.WorkRecord, ) (agentguard.IsolationDescriptor, error) { return loadOverlayDescriptor(reviewer.backend, work) } func loadOverlayDescriptor( backend retainedIsolationBackend, work agenttask.WorkRecord, ) (agentguard.IsolationDescriptor, error) { descriptor, _, err := loadOverlayState(backend, work) return descriptor, err } func loadOverlayState( backend retainedIsolationBackend, work agenttask.WorkRecord, ) (agentguard.IsolationDescriptor, agentworkspace.OverlayRecord, error) { if backend == nil { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, errors.New("taskloop: retained workspace backend is unavailable") } if work.Isolation == nil || work.Isolation.ID == "" || work.Isolation.TaskRoot == "" { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, errors.New("taskloop: reviewed work has no retained isolation") } recordPath := filepath.Join(work.Isolation.TaskRoot, "overlay.json") content, err := os.ReadFile(recordPath) if err != nil { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, fmt.Errorf("taskloop: read retained overlay: %w", err) } var record agentworkspace.OverlayRecord if err := decodeStrictJSON(content, &record); err != nil { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, fmt.Errorf("taskloop: decode retained overlay: %w", err) } descriptor := agentguard.IsolationDescriptor{ ID: record.IsolationID, Revision: record.Revision, Mode: record.Mode, BaseRoot: record.CanonicalRoot, TaskRoot: filepath.Dir(record.Locator.OverlayRecord), WorkingDir: record.Locator.ViewRoot, WritableRoots: []string{record.Locator.ViewRoot, record.Locator.TempRoot, record.Locator.CacheRoot}, PinnedBaseRevision: record.SnapshotRevision, ConfinementRevision: record.ConfinementRevision, } if record.ProjectID == "" || record.WorkUnitID != string(work.Unit.ID) || record.AttemptID != string(work.AttemptID) || record.IsolationID != work.Isolation.ID || record.Revision != work.Isolation.Revision { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, errors.New("taskloop: retained overlay identity mismatch") } loaded, err := backend.LoadRecord(descriptor) if err != nil { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, err } if !reflect.DeepEqual(loaded, record) { return agentguard.IsolationDescriptor{}, agentworkspace.OverlayRecord{}, errors.New("taskloop: retained overlay record identity mismatch") } return descriptor, record, nil } var errVerdictMissing = errors.New("taskloop: official review verdict is missing") func readReviewVerdict(path string) (agenttask.ReviewVerdict, string, error) { content, err := os.ReadFile(path) if err != nil { return "", "", err } if len(content) == 0 || len(content) > maxWorkflowArtifactBytes { return "", "", errors.New("taskloop: official review artifact is not bounded") } lines := strings.Split(string(content), "\n") heading := -1 for index, line := range lines { if strings.TrimSpace(line) == "## Code Review Result" { heading = index } } if heading < 0 { return "", "", errVerdictMissing } var values []string for _, line := range lines[heading+1:] { text := strings.TrimSpace(line) if strings.HasPrefix(text, "## ") { break } if strings.HasPrefix(text, "-") { text = strings.TrimSpace(strings.TrimPrefix(text, "-")) } field, value, ok := strings.Cut(text, ":") if ok && strings.TrimSpace(field) == "Overall Verdict" { values = append(values, strings.TrimSpace(value)) } } if len(values) == 0 { return "", "", errVerdictMissing } if len(values) != 1 { return "", "", errors.New("taskloop: official review verdict is ambiguous") } switch values[0] { case "PASS": return agenttask.ReviewVerdictPass, "official review passed", nil case "WARN": return agenttask.ReviewVerdictWarn, "official review requested follow-up", nil case "FAIL": return agenttask.ReviewVerdictFail, "official review failed", nil case "USER_REVIEW": return agenttask.ReviewVerdictUserReview, "official review requires user review", nil default: return "", "", errors.New("taskloop: official review verdict is invalid") } } // CatalogReviewExecutor rehydrates the retained isolation and starts the // official reviewer only through that exact executable confinement proof. type CatalogReviewExecutor struct { catalog agentconfig.Catalog backend retainedIsolationBackend } func NewCatalogReviewExecutor( catalogConfig agentconfig.Catalog, backend retainedIsolationBackend, ) (*CatalogReviewExecutor, error) { if backend == nil { return nil, errors.New("taskloop: official review requires a retained isolation backend") } normalized, err := agentconfig.Normalize(catalogConfig) if err != nil { return nil, err } return &CatalogReviewExecutor{catalog: normalized, backend: backend}, nil } func (executor *CatalogReviewExecutor) ExecuteReview( ctx context.Context, request agenttask.ReviewRequest, workspace, planRelative, reviewRelative string, ) error { if request.Work.Target == nil { return errors.New("taskloop: official review has no selected provider target") } prepared, descriptor, err := rehydrateRetainedIsolation( ctx, executor.backend, executor.catalog, request.Project, request.Work, ) if err != nil { return err } if workspace != descriptor.WorkingDir { return errors.New("taskloop: official review workspace is not the retained view") } resolved, ok := executor.catalog.ResolveProfile(request.Work.Target.ProfileID) if !ok { return errors.New("taskloop: official review profile is not declared") } prompt := officialReviewPrompt(planRelative, reviewRelative) command, _, err := prepareFreshCatalogCommand( resolved, *request.Work.Target, prepared.Confinement.Binding(), descriptor.WorkingDir, prompt, request.Project, request.Work, "official-review", ) if err != nil { return err } return invokeConfined(ctx, prepared.Confinement, command) } func officialReviewPrompt(plan, review string) string { return fmt.Sprintf( "This is a bounded official-review step owned by the task loop, not task "+ "finalization. Review %s against %s without implementing changes or "+ "invoking a task-loop dispatcher. Do not modify review-only checklists, "+ "write completion metadata, update a roadmap, or archive, move, rename, "+ "or delete either active artifact. Append only one `## Code Review Result` "+ "section to the existing review artifact, with exactly one "+ "`Overall Verdict:` field whose value is PASS, WARN, FAIL, or USER_REVIEW.", plan, review, ) } func rehydrateRetainedIsolation( ctx context.Context, backend retainedIsolationBackend, catalog agentconfig.Catalog, project agenttask.ProjectRecord, work agenttask.WorkRecord, ) (agenttask.PreparedIsolation, agentguard.IsolationDescriptor, error) { if work.Target == nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, errors.New("taskloop: retained work has no selected target") } descriptor, record, err := loadOverlayState(backend, work) if err != nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, err } if record.ProjectID != string(project.ProjectID) || record.WorkspaceID != string(project.WorkspaceID) || record.ConfigRevision != string(work.Target.ConfigRevision) || record.ProfileRevision != work.Target.ProfileRevision { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, errors.New("taskloop: retained overlay policy identity mismatch") } resolved, ok := catalog.ResolveProfile(work.Target.ProfileID) if !ok || validateCatalogTarget(resolved, *work.Target) != nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, errors.New("taskloop: retained target does not match the catalog") } prepared, err := backend.Prepare(ctx, agenttask.IsolationRequest{ Project: project, Work: work, Target: *work.Target, IdempotencyKey: record.IdempotencyKey, }) if err != nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, err } if prepared.Descriptor == nil || !reflect.DeepEqual(*prepared.Descriptor, descriptor) || !reflect.DeepEqual(prepared.Profile, guardProfile(resolved)) || prepared.Confinement == nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, errors.New("taskloop: rehydrated isolation identity mismatch") } binding := prepared.Confinement.Binding() if prepared.Confinement.Revision() != record.ConfinementRevision || binding.Revision != record.ConfinementRevision || binding.IsolationID != record.IsolationID || binding.IsolationRevision != record.Revision || binding.PinnedBaseRevision != record.SnapshotRevision || binding.ConfigRevision != record.ConfigRevision || binding.GrantRevision != record.GrantRevision || binding.ProfileRevision != record.ProfileRevision || binding.BaseRoot != record.CanonicalRoot || binding.SnapshotRoot != record.Locator.SnapshotRoot || binding.TaskRoot != descriptor.TaskRoot || binding.WorkingDir != descriptor.WorkingDir || !reflect.DeepEqual(binding.WritableRoots, descriptor.WritableRoots) { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, errors.New("taskloop: rehydrated confinement binding mismatch") } if err := prepared.Confinement.Validate(binding); err != nil { return agenttask.PreparedIsolation{}, agentguard.IsolationDescriptor{}, fmt.Errorf("taskloop: validate retained confinement: %w", err) } return prepared, descriptor, nil }