92 lines
2.7 KiB
Go
92 lines
2.7 KiB
Go
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
|
|
}
|
|
}
|