package taskloop import ( "context" "errors" "fmt" "os/exec" "path/filepath" "iop/packages/go/agenttask" "iop/packages/go/agentworkspace" ) // Integration keeps the standalone host boundary explicit while delegating all // immutable change-set ordering, apply, rollback, and retention behavior to the // shared workspace integrator. type Integration struct { shared *agentworkspace.SerialIntegrator } var _ agenttask.Integrator = (*Integration)(nil) func NewIntegration( backend *agentworkspace.Backend, store agentworkspace.IntegrationRecordStore, validator agentworkspace.ValidationFunc, ) (*Integration, error) { if validator == nil { return nil, errors.New("taskloop: post-apply validator is required") } shared, err := agentworkspace.NewIntegrator(agentworkspace.IntegratorConfig{ Backend: backend, Store: store, Validator: validator, }) if err != nil { return nil, err } return &Integration{shared: shared}, nil } func (integration *Integration) Integrate( ctx context.Context, request agenttask.IntegrationRequest, ) (agenttask.IntegrationResult, error) { if integration == nil || integration.shared == nil { return agenttask.IntegrationResult{}, errors.New("taskloop: shared integrator is unavailable") } return integration.shared.Integrate(ctx, request) } // DefaultValidator performs the standalone host's deterministic post-apply // check in the runtime-owned candidate root. It never runs a command in or // writes to CanonicalRoot. func DefaultValidator() agentworkspace.ValidationFunc { return func( ctx context.Context, request agentworkspace.ValidationRequest, ) error { if err := ctx.Err(); err != nil { return err } if !filepath.IsAbs(request.ValidationRoot) || filepath.Clean(request.ValidationRoot) != request.ValidationRoot || !filepath.IsAbs(request.CanonicalRoot) || filepath.Clean(request.CanonicalRoot) != request.CanonicalRoot || request.ValidationRoot == request.CanonicalRoot { return errors.New("taskloop: validation requires a distinct owned candidate root") } command := exec.CommandContext( ctx, "git", "diff", "--no-index", "--check", "--", request.CanonicalRoot, request.ValidationRoot, ) var stdout, stderr boundedBuffer command.Stdout = &stdout command.Stderr = &stderr if err := command.Run(); err != nil { var exitError *exec.ExitError // `git diff --no-index` uses exit 1 for an ordinary content // difference. Whitespace/check failures and command failures use // a larger exit code and must reject the candidate. if !errors.As(err, &exitError) || exitError.ExitCode() != 1 { return fmt.Errorf("taskloop: candidate validation command failed: %w", err) } } return nil } }