iop/apps/agent/internal/taskloop/integration.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
}
}