Repository: bacalhau-project/bacalhau (language: python)

Issue:
do our best to give more sensible error messages around docker registry
Example:
```
- Status: 'Could not inspect image "bacalhauproject/duckdb-log-processor:v0.17"
  - could be due to repo/image not existing, or registry needing authorization:
```

Image didn't exist (my fault) - we should try to make this friendly.


Discussion / hints:
Can you say more about how you'd expect this to be more friendly? It says in the error message "could be due to image not existing" which was indeed the cause?

Fix patch:
diff --git a/pkg/bidstrategy/chained.go b/pkg/bidstrategy/chained.go
index 364eb6f8fc..d18e8ca598 100644
--- a/pkg/bidstrategy/chained.go
+++ b/pkg/bidstrategy/chained.go
@@ -3,6 +3,7 @@ package bidstrategy
 import (
 	"context"
 	"reflect"
+	"strings"
 
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/rs/zerolog/log"
@@ -46,6 +47,8 @@ func (c *ChainedBidStrategy) AddStrategy(opts ...StrategyOpt) {
 // ShouldBid Iterate over all strategies, and return shouldBid if no error is thrown
 // and none of the strategies return should not bid.
 func (c *ChainedBidStrategy) ShouldBid(ctx context.Context, request BidStrategyRequest) (BidStrategyResponse, error) {
+	reasons := make([]string, 0, len(c.Semantics))
+
 	for _, strategy := range c.Semantics {
 		response, err := strategy.ShouldBid(ctx, request)
 		if err != nil {
@@ -64,15 +67,17 @@ func (c *ChainedBidStrategy) ShouldBid(ctx context.Context, request BidStrategyR
 				reflect.TypeOf(strategy).String(), status, response.Reason)
 			return response, nil
 		}
+		reasons = append(reasons, response.Reason)
 	}
 
-	return NewShouldBidResponse(), nil
+	return BidStrategyResponse{ShouldBid: true, Reason: strings.Join(reasons, "; ")}, nil
 }
 
 // ShouldBidBasedOnUsage Iterate over all strategies, and return shouldBid if no error is thrown
 // and none of the strategies return should not bid.
 func (c *ChainedBidStrategy) ShouldBidBasedOnUsage(
 	ctx context.Context, request BidStrategyRequest, usage models.Resources) (BidStrategyResponse, error) {
+	reasons := make([]string, 0, len(c.Resources))
 	for _, strategy := range c.Resources {
 		response, err := strategy.ShouldBidBasedOnUsage(ctx, request, usage)
 		if err != nil {
@@ -93,5 +98,8 @@ func (c *ChainedBidStrategy) ShouldBidBasedOnUsage(
 		}
 	}
 
-	return NewShouldBidResponse(), nil
+	return BidStrategyResponse{ShouldBid: true, Reason: strings.Join(reasons, "; ")}, nil
 }
+
+var _ SemanticBidStrategy = (*ChainedBidStrategy)(nil)
+var _ ResourceBidStrategy = (*ChainedBidStrategy)(nil)
diff --git a/pkg/bidstrategy/resource/capacity_available_strategy.go b/pkg/bidstrategy/resource/capacity_available_strategy.go
index bcb27ca264..0d7b4836b7 100644
--- a/pkg/bidstrategy/resource/capacity_available_strategy.go
+++ b/pkg/bidstrategy/resource/capacity_available_strategy.go
@@ -2,7 +2,6 @@ package resource
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
 	"github.com/bacalhau-project/bacalhau/pkg/compute/capacity"
@@ -19,6 +18,8 @@ type AvailableCapacityStrategy struct {
 	enqueuedCapacityTracker capacity.Tracker
 }
 
+const capacityReason = "have capacity to run the job (%s requested but only %s is available)"
+
 func NewAvailableCapacityStrategy(ctx context.Context, params AvailableCapacityStrategyParams) *AvailableCapacityStrategy {
 	s := &AvailableCapacityStrategy{
 		runningCapacityTracker:  params.RunningCapacityTracker,
@@ -33,14 +34,7 @@ func (s *AvailableCapacityStrategy) ShouldBidBasedOnUsage(
 	runningCapacity := s.runningCapacityTracker.GetAvailableCapacity(ctx)
 	enqueuedCapacity := s.enqueuedCapacityTracker.GetAvailableCapacity(ctx)
 	totalCapacity := runningCapacity.Add(enqueuedCapacity)
-	if !usage.LessThanEq(*totalCapacity) {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason:    fmt.Sprintf("not enough capacity available. Total: %s Requested: %s", totalCapacity, &usage),
-		}, nil
-	}
-
-	return bidstrategy.NewShouldBidResponse(), nil
+	return bidstrategy.NewBidResponse(usage.LessThanEq(*totalCapacity), capacityReason, totalCapacity, usage), nil
 }
 
 // compile-time interface check
diff --git a/pkg/bidstrategy/resource/capacity_max_strategy.go b/pkg/bidstrategy/resource/capacity_max_strategy.go
index 286f85e5f4..865695905a 100644
--- a/pkg/bidstrategy/resource/capacity_max_strategy.go
+++ b/pkg/bidstrategy/resource/capacity_max_strategy.go
@@ -21,17 +21,12 @@ func NewMaxCapacityStrategy(params MaxCapacityStrategyParams) *MaxCapacityStrate
 	}
 }
 
+const resourceReason = "run jobs that require this many resources (%s requested but only %s is allowed)"
+
 func (s *MaxCapacityStrategy) ShouldBidBasedOnUsage(
 	ctx context.Context, request bidstrategy.BidStrategyRequest, usage models.Resources) (bidstrategy.BidStrategyResponse, error) {
 	// skip bidding if we don't have enough capacity available
-	if !usage.LessThanEq(s.maxJobRequirements) {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason:    "job requirements exceed max allowed per job",
-		}, nil
-	}
-
-	return bidstrategy.NewShouldBidResponse(), nil
+	return bidstrategy.NewBidResponse(usage.LessThanEq(s.maxJobRequirements), resourceReason, usage, s.maxJobRequirements), nil
 }
 
 // compile-time interface check
diff --git a/pkg/bidstrategy/resource/chained.go b/pkg/bidstrategy/resource/chained.go
deleted file mode 100644
index 533aa3766d..0000000000
--- a/pkg/bidstrategy/resource/chained.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package resource
-
-import (
-	"context"
-	"reflect"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/rs/zerolog/log"
-
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-)
-
-type ChainedResourceBidStrategy struct {
-	Strategies []bidstrategy.ResourceBidStrategy
-}
-
-func NewChainedResourceBidStrategy(strategies ...bidstrategy.ResourceBidStrategy) *ChainedResourceBidStrategy {
-	return &ChainedResourceBidStrategy{Strategies: strategies}
-}
-
-// AddStrategy Add new strategy to the end of the chain
-func (c *ChainedResourceBidStrategy) AddStrategy(strategy bidstrategy.ResourceBidStrategy) {
-	c.Strategies = append(c.Strategies, strategy)
-}
-
-// ShouldBidBasedOnUsage Iterate over all strategies, and return shouldBid if no error is thrown
-// and none of the strategies return should not bid.
-func (c *ChainedResourceBidStrategy) ShouldBidBasedOnUsage(
-	ctx context.Context, request bidstrategy.BidStrategyRequest, usage models.Resources) (bidstrategy.BidStrategyResponse, error) {
-	return c.delegate(ctx, func(strategy bidstrategy.ResourceBidStrategy) (bidstrategy.BidStrategyResponse, error) {
-		return strategy.ShouldBidBasedOnUsage(ctx, request, usage)
-	})
-}
-
-func (c *ChainedResourceBidStrategy) delegate(
-	ctx context.Context,
-	f func(strategy bidstrategy.ResourceBidStrategy) (bidstrategy.BidStrategyResponse, error),
-) (bidstrategy.BidStrategyResponse, error) {
-	for _, strategy := range c.Strategies {
-		response, err := f(strategy)
-		if err != nil {
-			log.Ctx(ctx).Error().Err(err).Msgf("error asking bidding strategy %s if we should bid",
-				reflect.TypeOf(strategy).String())
-			return bidstrategy.BidStrategyResponse{}, err
-		}
-		var status string
-		if response.ShouldWait {
-			status = "should wait"
-		} else if !response.ShouldBid {
-			status = "should not bid"
-		}
-		if status != "" {
-			log.Ctx(ctx).Debug().Msgf("bidding strategy %s returned %s due to: %s",
-				reflect.TypeOf(strategy).String(), status, response.Reason)
-			return response, nil
-		}
-	}
-
-	return bidstrategy.BidStrategyResponse{ShouldBid: true}, nil
-}
diff --git a/pkg/bidstrategy/semantic/chained.go b/pkg/bidstrategy/semantic/chained.go
deleted file mode 100644
index a21652ec0e..0000000000
--- a/pkg/bidstrategy/semantic/chained.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package semantic
-
-import (
-	"context"
-	"reflect"
-
-	"github.com/rs/zerolog/log"
-
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-)
-
-type ChainedBidStrategy struct {
-	Strategies []bidstrategy.SemanticBidStrategy
-}
-
-func NewChainedSemanticBidStrategy(strategies ...bidstrategy.SemanticBidStrategy) *ChainedBidStrategy {
-	return &ChainedBidStrategy{Strategies: strategies}
-}
-
-// AddStrategy Add new strategy to the end of the chain
-func (c *ChainedBidStrategy) AddStrategy(strategy bidstrategy.SemanticBidStrategy) {
-	c.Strategies = append(c.Strategies, strategy)
-}
-
-// ShouldBid Iterate over all strategies, and return shouldBid if no error is thrown
-// and none of the strategies return should not bid.
-func (c *ChainedBidStrategy) ShouldBid(
-	ctx context.Context,
-	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
-	return c.delegate(ctx, func(strategy bidstrategy.SemanticBidStrategy) (bidstrategy.BidStrategyResponse, error) {
-		return strategy.ShouldBid(ctx, request)
-	})
-}
-
-func (c *ChainedBidStrategy) delegate(
-	ctx context.Context,
-	f func(strategy bidstrategy.SemanticBidStrategy) (bidstrategy.BidStrategyResponse, error),
-) (bidstrategy.BidStrategyResponse, error) {
-	for _, strategy := range c.Strategies {
-		response, err := f(strategy)
-		if err != nil {
-			log.Ctx(ctx).Error().Err(err).Msgf("error asking bidding strategy %s if we should bid",
-				reflect.TypeOf(strategy).String())
-			return bidstrategy.BidStrategyResponse{}, err
-		}
-		var status string
-		if response.ShouldWait {
-			status = "should wait"
-		} else if !response.ShouldBid {
-			status = "should not bid"
-		}
-		if status != "" {
-			log.Ctx(ctx).Debug().Msgf("bidding strategy %s returned %s due to: %s",
-				reflect.TypeOf(strategy).String(), status, response.Reason)
-			return response, nil
-		}
-	}
-
-	return bidstrategy.BidStrategyResponse{ShouldBid: true}, nil
-}
diff --git a/pkg/bidstrategy/semantic/distance_delay.go b/pkg/bidstrategy/semantic/distance_delay.go
deleted file mode 100644
index 609b342739..0000000000
--- a/pkg/bidstrategy/semantic/distance_delay.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package semantic
-
-import (
-	"context"
-	"hash/fnv"
-	"time"
-
-	"github.com/bacalhau-project/bacalhau/pkg/lib/math"
-	"github.com/rs/zerolog/log"
-
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-)
-
-// Decide whether we should even consider bidding on the job, early exit if
-// we're not in the active set for this job, given the hash distances.
-// (This is an optimization to avoid all nodes bidding on a job in large networks).
-
-type DistanceDelayStrategyParams struct {
-	NetworkSize int
-}
-
-// Compile-time check of interface implementation
-var _ bidstrategy.SemanticBidStrategy = (*DistanceDelayStrategy)(nil)
-
-type DistanceDelayStrategy struct {
-	networkSize int
-}
-
-func NewDistanceDelayStrategy(params DistanceDelayStrategyParams) *DistanceDelayStrategy {
-	return &DistanceDelayStrategy{networkSize: params.NetworkSize}
-}
-
-func (s DistanceDelayStrategy) ShouldBid(
-	ctx context.Context,
-	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
-	jobNodeDistanceDelayMs, shouldRunJob := s.calculateJobNodeDistanceDelay(ctx, request)
-	if !shouldRunJob {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason:    "Job to node hash distance too high",
-		}, nil
-	}
-
-	if jobNodeDistanceDelayMs > 0 {
-		log.Ctx(ctx).Debug().Msgf("Waiting %d ms before selecting job %s", jobNodeDistanceDelayMs, request.Job.ID)
-		time.Sleep(time.Millisecond * time.Duration(jobNodeDistanceDelayMs)) //nolint:gosec
-	}
-
-	return bidstrategy.NewShouldBidResponse(), nil
-}
-
-func (s DistanceDelayStrategy) calculateJobNodeDistanceDelay(ctx context.Context, request bidstrategy.BidStrategyRequest) (int, bool) {
-	// Calculate how long to wait to bid on the job by using a circular hashing
-	// style approach: Invent a metric for distance between node ID and job ID.
-	// If the node and job ID happen to be close to eachother, such that we'd
-	// expect that we are one of the N nodes "closest" to the job, bid
-	// instantly. Beyond that, back off an amount "stepped" proportional to how
-	// far we are from the job. This should evenly spread the work across the
-	// network, and have the property of on average only concurrency many nodes
-	// bidding on the job, and other nodes not bothering to bid because they
-	// will already have seen bid/bidaccepted messages from the close nodes.
-	// This will decrease overall network traffic, improving CPU and memory
-	// usage in large clusters.
-	nodeHash := hash(request.NodeID)
-	jobHash := hash(request.Job.ID)
-	// Range: 0 through 4,294,967,295. (4 billion)
-	distance := diff(nodeHash, jobHash)
-	// scale distance per chunk by concurrency (so that many nodes bid on a job
-	// with high concurrency). IOW, divide the space up into this many pieces.
-	// If concurrency=3 and network size=3, there'll only be one piece and
-	// everyone will bid. If concurrency=1 and network size=1 million, there
-	// will be a million slices of the hash space.
-	concurrency := math.Max(1, request.Job.Count)
-	chunk := int((float32(concurrency) / float32(s.networkSize)) * 4294967295) //nolint:gomnd
-	// wait 1 second per chunk distance. So, if we land in exactly the same
-	// chunk, bid immediately. If we're one chunk away, wait a bit before
-	// bidding. If we're very far away, wait a very long time.
-	delay := (distance / chunk) * 1000 //nolint:gomnd
-	log.Ctx(ctx).Trace().Msgf(
-		"node/job %s/%s, %d/%d, dist=%d, chunk=%d, delay=%d",
-		request.NodeID, request.Job.ID, nodeHash, jobHash, distance, chunk, delay,
-	)
-	shouldRun := true
-	// if delay is too high, just exit immediately.
-	if delay > 1000 { //nolint:gomnd
-		// drop the job on the floor, :-O
-		shouldRun = false
-		log.Ctx(ctx).Warn().Msgf(
-			"dropped job: node/job %s/%s, %d/%d, dist=%d, chunk=%d, delay=%d",
-			request.NodeID, request.Job.ID, nodeHash, jobHash, distance, chunk, delay,
-		)
-	}
-	return delay, shouldRun
-}
-
-func hash(s string) int {
-	h := fnv.New32a()
-	h.Write([]byte(s))
-	return int(h.Sum32())
-}
-
-func diff(a, b int) int {
-	if a < b {
-		return b - a
-	}
-	return a - b
-}
diff --git a/pkg/bidstrategy/semantic/external_exec.go b/pkg/bidstrategy/semantic/external_exec.go
index 8ad2deb64e..0ed5ad96dd 100644
--- a/pkg/bidstrategy/semantic/external_exec.go
+++ b/pkg/bidstrategy/semantic/external_exec.go
@@ -32,11 +32,15 @@ func NewExternalCommandStrategy(params ExternalCommandStrategyParams) *ExternalC
 	}
 }
 
+const (
+	exitCodeReason = "accept jobs where external command %q returns exit code %d"
+)
+
 func (s *ExternalCommandStrategy) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
 	if s.command == "" {
-		return bidstrategy.NewShouldBidResponse(), nil
+		return bidstrategy.NewBidResponse(true, notConfiguredReason), nil
 	}
 
 	// TODO: Use context to trace exec call
@@ -65,11 +69,5 @@ func (s *ExternalCommandStrategy) ShouldBid(
 	}
 
 	exitCode := cmd.ProcessState.ExitCode()
-	if exitCode == 0 {
-		return bidstrategy.NewShouldBidResponse(), nil
-	}
-	return bidstrategy.BidStrategyResponse{
-		ShouldBid: false,
-		Reason:    fmt.Sprintf("command `%s` returned non-zero exit code %d", s.command, exitCode),
-	}, nil
+	return bidstrategy.NewBidResponse(exitCode == 0, exitCodeReason, exitCode), nil
 }
diff --git a/pkg/bidstrategy/semantic/external_http.go b/pkg/bidstrategy/semantic/external_http.go
index 23b8df64f7..4994f27ea0 100644
--- a/pkg/bidstrategy/semantic/external_http.go
+++ b/pkg/bidstrategy/semantic/external_http.go
@@ -32,11 +32,16 @@ func NewExternalHTTPStrategy(params ExternalHTTPStrategyParams) *ExternalHTTPStr
 	}
 }
 
+const (
+	notConfiguredReason = "have HTTP moderation unconfigured"
+	statusCodeReason    = "accept jobs where url %q returns HTTP status code %d"
+)
+
 func (s *ExternalHTTPStrategy) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
 	if s.url == "" {
-		return bidstrategy.NewShouldBidResponse(), nil
+		return bidstrategy.NewBidResponse(true, notConfiguredReason), nil
 	}
 
 	data := bidstrategy.GetJobSelectionPolicyProbeData(request)
@@ -59,15 +64,16 @@ func (s *ExternalHTTPStrategy) ShouldBid(
 	}
 	defer closer.DrainAndCloseWithLogOnError(ctx, s.url, resp.Body)
 
+	// HTTP call returned an error code of some sort - treat this as a job
+	// rejection.
 	if resp.StatusCode >= http.StatusBadRequest {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason:    fmt.Sprintf("url `%s` returned %d status code", s.url, resp.StatusCode),
-		}, nil
+		return bidstrategy.NewBidResponse(false, statusCodeReason, s.url, resp.StatusCode), nil
 	}
 
+	// HTTP call returned a positive status code. If it didn't return JSON, we
+	// can just accept now. If it did, it should return a JSON bid response.
 	if resp.Header.Get("Content-Type") != "application/json" {
-		return bidstrategy.NewShouldBidResponse(), nil
+		return bidstrategy.NewBidResponse(true, statusCodeReason, s.url, resp.StatusCode), nil
 	}
 
 	if resp.ContentLength > int64(marshaller.MaxSerializedStringInput) {
diff --git a/pkg/bidstrategy/semantic/input_locality.go b/pkg/bidstrategy/semantic/input_locality.go
index 68b20c0a81..d6b8a83aec 100644
--- a/pkg/bidstrategy/semantic/input_locality.go
+++ b/pkg/bidstrategy/semantic/input_locality.go
@@ -28,17 +28,23 @@ func NewInputLocalityStrategy(params InputLocalityStrategyParams) *InputLocality
 	}
 }
 
+const (
+	anywhereReason = "download data that is not available locally"
+	nonLocalReason = "have all input data locally (%d inputs require downloading)"
+)
+
 func (s *InputLocalityStrategy) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest,
 ) (bidstrategy.BidStrategyResponse, error) {
 	// if we have an "anywhere" policy for the data then we accept the job
 	if s.locality == Anywhere {
-		return bidstrategy.NewShouldBidResponse(), nil
+		return bidstrategy.NewBidResponse(true, anywhereReason), nil
 	}
 
+	inputSources := request.Job.Task().InputSources
 	foundInputs := 0
-	for _, input := range request.Job.Task().InputSources {
+	for _, input := range inputSources {
 		// see if the storage engine reports that we have the resource locally
 		strg, err := s.storages.Get(ctx, input.Source.Type)
 		if err != nil {
@@ -53,8 +59,5 @@ func (s *InputLocalityStrategy) ShouldBid(
 		}
 	}
 
-	if foundInputs >= len(request.Job.Task().InputSources) {
-		return bidstrategy.NewShouldBidResponse(), nil
-	}
-	return bidstrategy.BidStrategyResponse{ShouldBid: false, Reason: "not all inputs are local"}, nil
+	return bidstrategy.NewBidResponse(foundInputs >= len(inputSources), nonLocalReason, len(inputSources)-foundInputs), nil
 }
diff --git a/pkg/bidstrategy/semantic/input_stateless.go b/pkg/bidstrategy/semantic/input_stateless.go
index 581adbba4a..b86e45216f 100644
--- a/pkg/bidstrategy/semantic/input_stateless.go
+++ b/pkg/bidstrategy/semantic/input_stateless.go
@@ -23,13 +23,18 @@ func NewStatelessJobStrategy(params StatelessJobStrategyParams) *StatelessJobStr
 	}
 }
 
+const (
+	statelessReason = "accept jobs without any input volumes"
+	statefulReason  = "accept jobs with input volumes"
+)
+
 func (s *StatelessJobStrategy) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
 	// skip bidding if no input data is provided, and policy is to reject stateless jobs
-	if s.rejectStatelessJobs && len(request.Job.Task().InputSources) == 0 {
-		return bidstrategy.BidStrategyResponse{ShouldBid: false, Reason: "stateless jobs not accepted"}, nil
+	if len(request.Job.Task().InputSources) > 0 {
+		return bidstrategy.NewBidResponse(true, statefulReason), nil
+	} else {
+		return bidstrategy.NewBidResponse(!s.rejectStatelessJobs, statelessReason), nil
 	}
-
-	return bidstrategy.NewShouldBidResponse(), nil
 }
diff --git a/pkg/bidstrategy/semantic/networking.go b/pkg/bidstrategy/semantic/networking.go
index 3fb0c90760..a356a60f30 100644
--- a/pkg/bidstrategy/semantic/networking.go
+++ b/pkg/bidstrategy/semantic/networking.go
@@ -2,7 +2,6 @@ package semantic
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
 )
@@ -17,13 +16,19 @@ func NewNetworkingStrategy(accept bool) *NetworkingStrategy {
 	return &NetworkingStrategy{accept}
 }
 
+const (
+	docsLink        = "(see https://docs.bacalhau.org/next-steps/networking)"
+	accessReason    = "run jobs that require network access " + docsLink
+	localOnlyReason = "run jobs that do not require network access " + docsLink
+)
+
 // ShouldBid implements BidStrategy
 func (s *NetworkingStrategy) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
-	shouldBid := s.Accept || request.Job.Task().Network.Disabled()
-	return bidstrategy.BidStrategyResponse{
-		ShouldBid: shouldBid,
-		Reason:    fmt.Sprintf("networking is enabled: %t", s.Accept),
-	}, nil
+	if request.Job.Task().Network.Disabled() {
+		return bidstrategy.NewBidResponse(true, localOnlyReason), nil
+	}
+
+	return bidstrategy.NewBidResponse(s.Accept, accessReason), nil
 }
diff --git a/pkg/bidstrategy/semantic/provider_installed.go b/pkg/bidstrategy/semantic/provider_installed.go
index 43b67340c7..08a59ebbe3 100644
--- a/pkg/bidstrategy/semantic/provider_installed.go
+++ b/pkg/bidstrategy/semantic/provider_installed.go
@@ -2,7 +2,6 @@ package semantic
 
 import (
 	"context"
-	"fmt"
 	"strings"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
@@ -47,14 +46,13 @@ func (s *ProviderInstalledStrategy[P]) ShouldBid(
 	ctx context.Context,
 	request bidstrategy.BidStrategyRequest,
 ) (resp bidstrategy.BidStrategyResponse, err error) {
-	resp.ShouldBid = true
-	for _, key := range s.getter(&request.Job) {
-		resp.ShouldBid = s.provider.Has(ctx, key)
-		resp.Reason = fmt.Sprintf("%s installed: %t", key, resp.ShouldBid)
-		if !resp.ShouldBid {
-			return
+	keys := s.getter(&request.Job)
+	for _, key := range keys {
+		supportsKey := s.provider.Has(ctx, key)
+		if !supportsKey {
+			return bidstrategy.NewBidResponse(false, "support %q", key), nil
 		}
 	}
 
-	return
+	return bidstrategy.NewBidResponse(true, "support %v", keys), nil
 }
diff --git a/pkg/bidstrategy/semantic/timeout_strategy.go b/pkg/bidstrategy/semantic/timeout_strategy.go
index c37e6af018..c37852231d 100644
--- a/pkg/bidstrategy/semantic/timeout_strategy.go
+++ b/pkg/bidstrategy/semantic/timeout_strategy.go
@@ -2,7 +2,6 @@ package semantic
 
 import (
 	"context"
-	"fmt"
 	"time"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
@@ -32,9 +31,16 @@ func NewTimeoutStrategy(params TimeoutStrategyParams) *TimeoutStrategy {
 	}
 }
 
+const (
+	maxReason    = "accept jobs with timeout %v (the maximum allowed is %v)"
+	minReason    = "accept jobs with timeout %v (the minimum allowed is %v)"
+	bypassReason = "allow client %q to bypass timeout limits" //nolint:gosec
+)
+
 func (s *TimeoutStrategy) ShouldBid(_ context.Context, request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
-	if request.Job.Task().Timeouts.ExecutionTimeout <= 0 {
-		return bidstrategy.NewShouldBidResponse(), nil
+	timeoutSeconds := request.Job.Task().Timeouts.ExecutionTimeout
+	if timeoutSeconds <= 0 {
+		return bidstrategy.NewBidResponse(true, minReason, timeoutSeconds, 0), nil
 	}
 
 	// Timeout will be multiplied by 1000000000 (time.Second) when it gets
@@ -42,33 +48,21 @@ func (s *TimeoutStrategy) ShouldBid(_ context.Context, request bidstrategy.BidSt
 	// that it can fit into it.
 	var maxTimeout = int64(model.NoJobTimeout.Seconds())
 	if request.Job.Task().Timeouts.ExecutionTimeout > maxTimeout {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason: fmt.Sprintf("job timeout %d exceeds maximum possible value %d",
-				request.Job.Task().Timeouts.ExecutionTimeout, maxTimeout),
-		}, nil
+		return bidstrategy.NewBidResponse(false, maxReason, timeoutSeconds, maxTimeout), nil
 	}
 
 	for _, clientID := range s.jobExecutionTimeoutClientIDBypassList {
 		if request.Job.Namespace == clientID {
-			return bidstrategy.NewShouldBidResponse(), nil
+			return bidstrategy.NewBidResponse(true, bypassReason, clientID), nil
 		}
 	}
 
 	// skip bidding if the job spec defined a timeout value higher or lower than what we are willing to accept
-	if s.maxJobExecutionTimeout > 0 && request.Job.Task().Timeouts.GetExecutionTimeout() > s.maxJobExecutionTimeout {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason: fmt.Sprintf("job timeout %s exceeds maximum allowed %s",
-				request.Job.Task().Timeouts.GetExecutionTimeout(), s.maxJobExecutionTimeout),
-		}, nil
+	timeoutDuration := request.Job.Task().Timeouts.GetExecutionTimeout()
+	if timeoutDuration < s.minJobExecutionTimeout {
+		return bidstrategy.NewBidResponse(false, minReason, timeoutDuration.String(), s.minJobExecutionTimeout.String()), nil
 	}
-	if request.Job.Task().Timeouts.GetExecutionTimeout() < s.minJobExecutionTimeout {
-		return bidstrategy.BidStrategyResponse{
-			ShouldBid: false,
-			Reason: fmt.Sprintf("job timeout %s below minimum allowed %s",
-				request.Job.Task().Timeouts.GetExecutionTimeout(), s.minJobExecutionTimeout),
-		}, nil
-	}
-	return bidstrategy.NewShouldBidResponse(), nil
+
+	success := s.maxJobExecutionTimeout <= 0 || (s.maxJobExecutionTimeout > 0 && timeoutDuration <= s.maxJobExecutionTimeout)
+	return bidstrategy.NewBidResponse(success, maxReason, timeoutDuration.String(), s.maxJobExecutionTimeout.String()), nil
 }
diff --git a/pkg/bidstrategy/type.go b/pkg/bidstrategy/type.go
index 65e15d6366..215530a4cf 100644
--- a/pkg/bidstrategy/type.go
+++ b/pkg/bidstrategy/type.go
@@ -3,6 +3,7 @@ package bidstrategy
 
 import (
 	"context"
+	"fmt"
 	"net/url"
 
 	"github.com/bacalhau-project/bacalhau/pkg/models"
@@ -20,9 +21,23 @@ type BidStrategyResponse struct {
 	Reason     string `json:"reason"`
 }
 
-func NewShouldBidResponse() BidStrategyResponse {
+const (
+	reasonPrefix   string = "this node does "
+	reasonNegation string = "not "
+)
+
+func FormatReason(success bool, jobRequiresNodeTo string, fmtArgs ...any) string {
+	msg := reasonPrefix
+	if !success {
+		msg += reasonNegation
+	}
+	return fmt.Sprintf(msg+jobRequiresNodeTo, fmtArgs...)
+}
+
+func NewBidResponse(success bool, jobRequiresNodeTo string, fmtArgs ...any) BidStrategyResponse {
 	return BidStrategyResponse{
-		ShouldBid: true,
+		ShouldBid: success,
+		Reason:    FormatReason(success, jobRequiresNodeTo, fmtArgs...),
 	}
 }
 
diff --git a/pkg/executor/docker/bidstrategy/semantic/image_platform.go b/pkg/executor/docker/bidstrategy/semantic/image_platform.go
index 7b50923d8d..6d1172d423 100644
--- a/pkg/executor/docker/bidstrategy/semantic/image_platform.go
+++ b/pkg/executor/docker/bidstrategy/semantic/image_platform.go
@@ -36,7 +36,7 @@ func (s *ImagePlatformBidStrategy) ShouldBid(
 	request bidstrategy.BidStrategyRequest,
 ) (bidstrategy.BidStrategyResponse, error) {
 	if request.Job.Task().Engine.Type != models.EngineDocker {
-		return bidstrategy.NewShouldBidResponse(), nil
+		return bidstrategy.NewBidResponse(true, "examine images for non-Docker jobs"), nil
 	}
 
 	supported, serr := s.client.SupportedPlatforms(ctx)
@@ -92,16 +92,25 @@ func (s *ImagePlatformBidStrategy) ShouldBid(
 		}, nil
 	}
 
+	imageHasPlatforms := make([]string, 0, len(manifest.Platforms))
+	for _, imageHas := range manifest.Platforms {
+		imageHasPlatforms = append(imageHasPlatforms, imageHas.OS+"/"+imageHas.Architecture)
+	}
+
+	canRunPlatforms := make([]string, 0, len(supported))
+	for _, canRun := range supported {
+		canRunPlatforms = append(canRunPlatforms, canRun.OS+"/"+canRun.Architecture)
+	}
+
+	shouldBid := false
 	for _, canRun := range supported {
 		for _, imageHas := range manifest.Platforms {
 			if canRun.OS == imageHas.OS && canRun.Architecture == imageHas.Architecture {
-				return bidstrategy.NewShouldBidResponse(), nil
+				shouldBid = true
 			}
 		}
 	}
 
-	return bidstrategy.BidStrategyResponse{
-		ShouldBid: false,
-		Reason:    "Node does not support any of the published image platforms",
-	}, nil
+	const platformReason = "support the available image platforms %v (supports %v)"
+	return bidstrategy.NewBidResponse(shouldBid, platformReason, imageHasPlatforms, canRunPlatforms), nil
 }
diff --git a/pkg/executor/docker/executor.go b/pkg/executor/docker/executor.go
index 5013a3324a..282ca98b76 100644
--- a/pkg/executor/docker/executor.go
+++ b/pkg/executor/docker/executor.go
@@ -20,7 +20,6 @@ import (
 	pkgUtil "github.com/bacalhau-project/bacalhau/pkg/util"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy/resource"
 	"github.com/bacalhau-project/bacalhau/pkg/config"
 	"github.com/bacalhau-project/bacalhau/pkg/docker"
 	"github.com/bacalhau-project/bacalhau/pkg/executor"
@@ -101,8 +100,7 @@ func (e *Executor) ShouldBidBasedOnUsage(
 	request bidstrategy.BidStrategyRequest,
 	usage models.Resources,
 ) (bidstrategy.BidStrategyResponse, error) {
-	// TODO(forrest): should this just return true always?
-	return resource.NewChainedResourceBidStrategy().ShouldBidBasedOnUsage(ctx, request, usage)
+	return bidstrategy.NewBidResponse(true, "not place additional requirements on Docker jobs"), nil
 }
 
 // Start initiates an execution based on the provided RunCommandRequest.
diff --git a/pkg/executor/noop/executor.go b/pkg/executor/noop/executor.go
index 18d54ff968..c2e5d8ac1a 100644
--- a/pkg/executor/noop/executor.go
+++ b/pkg/executor/noop/executor.go
@@ -9,7 +9,6 @@ import (
 	"github.com/rs/zerolog/log"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy/semantic"
 	"github.com/bacalhau-project/bacalhau/pkg/executor"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/util/generic"
@@ -188,7 +187,7 @@ func (e *NoopExecutor) ShouldBid(ctx context.Context, request bidstrategy.BidStr
 		}
 		return strategy.ShouldBid(ctx, request)
 	}
-	return semantic.NewChainedSemanticBidStrategy().ShouldBid(ctx, request)
+	return bidstrategy.NewBidResponse(true, ""), nil
 }
 
 func (e *NoopExecutor) ShouldBidBasedOnUsage(
@@ -205,7 +204,7 @@ func (e *NoopExecutor) ShouldBidBasedOnUsage(
 		return strategy.ShouldBidBasedOnUsage(ctx, request, usage)
 	}
 	// TODO(forrest): [correctness] this returns the correct response, but could be made specific to this method.
-	return semantic.NewChainedSemanticBidStrategy().ShouldBid(ctx, request)
+	return bidstrategy.NewBidResponse(true, ""), nil
 }
 
 func (e *NoopExecutor) Run(
diff --git a/pkg/executor/wasm/executor.go b/pkg/executor/wasm/executor.go
index f323012a1b..827a138cbe 100644
--- a/pkg/executor/wasm/executor.go
+++ b/pkg/executor/wasm/executor.go
@@ -18,8 +18,6 @@ import (
 	"github.com/bacalhau-project/bacalhau/pkg/lib/math"
 
 	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy/resource"
-	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy/semantic"
 	"github.com/bacalhau-project/bacalhau/pkg/executor"
 	wasmmodels "github.com/bacalhau-project/bacalhau/pkg/executor/wasm/models"
 	wasmlogs "github.com/bacalhau-project/bacalhau/pkg/logger/wasm"
@@ -46,7 +44,7 @@ func (e *Executor) IsInstalled(context.Context) (bool, error) {
 }
 
 func (*Executor) ShouldBid(ctx context.Context, request bidstrategy.BidStrategyRequest) (bidstrategy.BidStrategyResponse, error) {
-	return semantic.NewChainedSemanticBidStrategy().ShouldBid(ctx, request)
+	return bidstrategy.NewBidResponse(true, "not place additional requirements on WASM jobs"), nil
 }
 
 func (*Executor) ShouldBidBasedOnUsage(
@@ -54,7 +52,7 @@ func (*Executor) ShouldBidBasedOnUsage(
 	request bidstrategy.BidStrategyRequest,
 	usage models.Resources,
 ) (bidstrategy.BidStrategyResponse, error) {
-	return resource.NewChainedResourceBidStrategy().ShouldBidBasedOnUsage(ctx, request, usage)
+	return bidstrategy.NewBidResponse(true, "not place additional requirements on WASM jobs"), nil
 }
 
 // Start initiates an execution based on the provided RunCommandRequest.
diff --git a/pkg/node/compute.go b/pkg/node/compute.go
index 913cd11b1f..3ece85edcd 100644
--- a/pkg/node/compute.go
+++ b/pkg/node/compute.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/url"
 
+	"github.com/bacalhau-project/bacalhau/pkg/bidstrategy"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi"
 	compute_endpoint "github.com/bacalhau-project/bacalhau/pkg/publicapi/endpoint/compute"
@@ -132,46 +133,40 @@ func NewComputeNode(
 		},
 	})
 
-	semanticBidStrat := config.BidSemanticStrategy
-	if semanticBidStrat == nil {
-		semanticBidStrat = semantic.NewChainedSemanticBidStrategy(
-			executor_util.NewExecutorSpecificBidStrategy(executors),
+	semanticBidStrat := bidstrategy.WithSemantics(config.BidSemanticStrategy)
+	if config.BidSemanticStrategy == nil {
+		semanticBidStrat = bidstrategy.WithSemantics(
 			semantic.NewNetworkingStrategy(config.JobSelectionPolicy.AcceptNetworkedJobs),
-			semantic.NewExternalCommandStrategy(semantic.ExternalCommandStrategyParams{
-				Command: config.JobSelectionPolicy.ProbeExec,
-			}),
-			semantic.NewExternalHTTPStrategy(semantic.ExternalHTTPStrategyParams{
-				URL: config.JobSelectionPolicy.ProbeHTTP,
+			semantic.NewTimeoutStrategy(semantic.TimeoutStrategyParams{
+				MaxJobExecutionTimeout:                config.MaxJobExecutionTimeout,
+				MinJobExecutionTimeout:                config.MinJobExecutionTimeout,
+				JobExecutionTimeoutClientIDBypassList: config.JobExecutionTimeoutClientIDBypassList,
 			}),
 			semantic.NewStatelessJobStrategy(semantic.StatelessJobStrategyParams{
 				RejectStatelessJobs: config.JobSelectionPolicy.RejectStatelessJobs,
 			}),
-			semantic.NewInputLocalityStrategy(semantic.InputLocalityStrategyParams{
-				Locality: config.JobSelectionPolicy.Locality,
-				Storages: storages,
-			}),
 			semantic.NewProviderInstalledStrategy(
 				publishers,
 				func(j *models.Job) string { return j.Task().Publisher.Type },
 			),
 			semantic.NewStorageInstalledBidStrategy(storages),
-			semantic.NewTimeoutStrategy(semantic.TimeoutStrategyParams{
-				MaxJobExecutionTimeout:                config.MaxJobExecutionTimeout,
-				MinJobExecutionTimeout:                config.MinJobExecutionTimeout,
-				JobExecutionTimeoutClientIDBypassList: config.JobExecutionTimeoutClientIDBypassList,
+			semantic.NewInputLocalityStrategy(semantic.InputLocalityStrategyParams{
+				Locality: config.JobSelectionPolicy.Locality,
+				Storages: storages,
+			}),
+			semantic.NewExternalCommandStrategy(semantic.ExternalCommandStrategyParams{
+				Command: config.JobSelectionPolicy.ProbeExec,
 			}),
-			// TODO XXX: don't hardcode networkSize, calculate this dynamically from
-			//  libp2p instead somehow. https://github.com/bacalhau-project/bacalhau/issues/512
-			semantic.NewDistanceDelayStrategy(semantic.DistanceDelayStrategyParams{
-				NetworkSize: 1,
+			semantic.NewExternalHTTPStrategy(semantic.ExternalHTTPStrategyParams{
+				URL: config.JobSelectionPolicy.ProbeHTTP,
 			}),
+			executor_util.NewExecutorSpecificBidStrategy(executors),
 		)
 	}
 
-	resourceBidStrat := config.BidResourceStrategy
-	if resourceBidStrat == nil {
-		resourceBidStrat = resource.NewChainedResourceBidStrategy(
-			executor_util.NewExecutorSpecificBidStrategy(executors),
+	resourceBidStrat := bidstrategy.WithResources(config.BidResourceStrategy)
+	if config.BidResourceStrategy == nil {
+		resourceBidStrat = bidstrategy.WithResources(
 			resource.NewMaxCapacityStrategy(resource.MaxCapacityStrategyParams{
 				MaxJobRequirements: config.JobResourceLimits,
 			}),
@@ -179,6 +174,7 @@ func NewComputeNode(
 				RunningCapacityTracker:  runningCapacityTracker,
 				EnqueuedCapacityTracker: enqueuedCapacityTracker,
 			}),
+			executor_util.NewExecutorSpecificBidStrategy(executors),
 		)
 	}
 
@@ -206,10 +202,11 @@ func NewComputeNode(
 		MaxJobRequirements: config.JobResourceLimits,
 	})
 
+	bidStrat := bidstrategy.NewChainedBidStrategy(semanticBidStrat, resourceBidStrat)
 	bidder := compute.NewBidder(compute.BidderParams{
 		NodeID:           host.ID().String(),
-		SemanticStrategy: semanticBidStrat,
-		ResourceStrategy: resourceBidStrat,
+		SemanticStrategy: bidStrat,
+		ResourceStrategy: bidStrat,
 		Store:            executionStore,
 		Callback:         computeCallback,
 		Executor:         bufferRunner,


Test patch:
diff --git a/pkg/bidstrategy/semantic/external_http_test.go b/pkg/bidstrategy/semantic/external_http_test.go
index cb97301b7e..8eeac52212 100644
--- a/pkg/bidstrategy/semantic/external_http_test.go
+++ b/pkg/bidstrategy/semantic/external_http_test.go
@@ -91,7 +91,7 @@ func TestJobSelectionHttp(t *testing.T) {
 			request := getBidStrategyRequest(t)
 			result, err := strategy.ShouldBid(context.Background(), request)
 			require.NoError(t, err)
-			require.Equal(t, test.expectBid, result.ShouldBid)
+			require.Equal(t, test.expectBid, result.ShouldBid, result.Reason)
 			require.Equal(t, test.expectWait, result.ShouldWait)
 
 			// this makes sure that the http payload was given to the http endpoint
diff --git a/pkg/bidstrategy/semantic/timeout_strategy_test.go b/pkg/bidstrategy/semantic/timeout_strategy_test.go
index 8eaddc3f8b..da0741a87c 100644
--- a/pkg/bidstrategy/semantic/timeout_strategy_test.go
+++ b/pkg/bidstrategy/semantic/timeout_strategy_test.go
@@ -41,7 +41,7 @@ func TestTimeoutStrategy(t *testing.T) {
 				},
 			},
 			shouldBid: false,
-			reason:    "job timeout 9223372037 exceeds maximum possible value 9223372036",
+			reason:    "this node does not accept jobs with timeout 9223372037 (the maximum allowed is 9223372036)",
 		},
 		{
 			name: "client-skip-list",
@@ -61,7 +61,7 @@ func TestTimeoutStrategy(t *testing.T) {
 				},
 			},
 			shouldBid: true,
-			reason:    "",
+			reason:    "this node does allow client \"client\" to bypass timeout limits",
 		},
 	}