Harden checkpoint reuse and combat validation
This commit is contained in:
@@ -97,18 +97,18 @@ type CheckpointDecision struct {
|
||||
ReasonCode CheckpointReasonCode `json:"reason_code,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
// Reason is retained as a compatibility/debug field for existing callers.
|
||||
// New checkpoint stores should put bounded, non-sensitive text in Detail.
|
||||
// Detail and Reason are derived from the stable reason code.
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
const checkpointDecisionDetailLimit = 512
|
||||
|
||||
func NewCheckpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode, detail string) CheckpointDecision {
|
||||
return checkpointDecision(category, reasonCode, detail)
|
||||
func NewCheckpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode) CheckpointDecision {
|
||||
return checkpointDecision(category, reasonCode)
|
||||
}
|
||||
|
||||
func checkpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode, detail string) CheckpointDecision {
|
||||
detail = sanitizeCheckpointDecisionDetail(detail)
|
||||
func checkpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode) CheckpointDecision {
|
||||
detail := normalizeCheckpointDecisionDetail(checkpointDecisionDetail(reasonCode))
|
||||
return CheckpointDecision{
|
||||
Reused: category == CheckpointDecisionReused,
|
||||
Category: category,
|
||||
@@ -118,12 +118,55 @@ func checkpointDecision(category CheckpointDecisionCategory, reasonCode Checkpoi
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeCheckpointDecisionDetail(detail string) string {
|
||||
detail = strings.TrimSpace(strings.ToValidUTF8(detail, "?"))
|
||||
lower := strings.ToLower(detail)
|
||||
if strings.ContainsAny(detail, `/\\`) || strings.Contains(lower, "secret") || strings.Contains(lower, "token") || strings.Contains(lower, "password") || strings.Contains(lower, "credential") || strings.Contains(lower, "environment") {
|
||||
return "checkpoint decision detail redacted"
|
||||
func checkpointDecisionDetail(reasonCode CheckpointReasonCode) string {
|
||||
switch reasonCode {
|
||||
case CheckpointReasonLoadingDisabled:
|
||||
return "checkpoint loading is disabled"
|
||||
case CheckpointReasonMissing:
|
||||
return "checkpoint artifact is missing"
|
||||
case CheckpointReasonPathInvalid:
|
||||
return "checkpoint location is invalid"
|
||||
case CheckpointReasonReadFailed:
|
||||
return "checkpoint artifact could not be read"
|
||||
case CheckpointReasonDecodeFailed:
|
||||
return "checkpoint artifact could not be decoded"
|
||||
case CheckpointReasonWorkspaceSchemaIncompatible:
|
||||
return "checkpoint workspace schema is incompatible"
|
||||
case CheckpointReasonIdentityMismatch:
|
||||
return "checkpoint identity does not match the current invocation"
|
||||
case CheckpointReasonStageMismatch:
|
||||
return "checkpoint stage does not match"
|
||||
case CheckpointReasonStepMismatch:
|
||||
return "checkpoint step does not match"
|
||||
case CheckpointReasonLaneMismatch:
|
||||
return "checkpoint lane does not match"
|
||||
case CheckpointReasonModuleMismatch:
|
||||
return "checkpoint module does not match"
|
||||
case CheckpointReasonStatusNotReusable:
|
||||
return "checkpoint status is not reusable"
|
||||
case CheckpointReasonDependencyMismatch:
|
||||
return "checkpoint dependencies do not match"
|
||||
case CheckpointReasonArtifactPayloadInvalid:
|
||||
return "checkpoint artifact payload is invalid"
|
||||
case CheckpointReasonArtifactDigestMismatch:
|
||||
return "checkpoint artifact digest does not match"
|
||||
case CheckpointReasonArtifactCodecIncompatible:
|
||||
return "checkpoint artifact is incompatible with the registered codec"
|
||||
case CheckpointReasonArtifactNotCanonical:
|
||||
return "checkpoint artifact is not canonical"
|
||||
case CheckpointReasonReused:
|
||||
return "checkpoint is reusable"
|
||||
case CheckpointReasonAcceptedArtifactReused:
|
||||
return "accepted normalized artifact is reusable"
|
||||
case CheckpointReasonRecomputeStep:
|
||||
return "selected step requires execution"
|
||||
default:
|
||||
return "checkpoint decision"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCheckpointDecisionDetail(detail string) string {
|
||||
detail = strings.TrimSpace(strings.ToValidUTF8(detail, "?"))
|
||||
var b strings.Builder
|
||||
for _, r := range detail {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
@@ -170,7 +213,7 @@ func (policy CheckpointExecutionPolicy) requiresReusable(stepID, laneID string)
|
||||
|
||||
func forceCheckpointDecision(policy CheckpointExecutionPolicy, stepID, laneID string, decision CheckpointDecision) CheckpointDecision {
|
||||
if policy.forced(stepID, laneID) {
|
||||
return checkpointDecision(CheckpointDecisionForcedRecompute, CheckpointReasonRecomputeStep, "selected step requires execution")
|
||||
return checkpointDecision(CheckpointDecisionForcedRecompute, CheckpointReasonRecomputeStep)
|
||||
}
|
||||
return decision
|
||||
}
|
||||
@@ -184,23 +227,38 @@ func requireReusableCheckpoint(policy CheckpointExecutionPolicy, stepID, laneID
|
||||
|
||||
// resolveCheckpointDecision applies runner policy and canonical payload
|
||||
// validation at the single point where a stage's observable decision is made.
|
||||
func resolveCheckpointDecision(output *RunOutput, loader CheckpointLoader, policy CheckpointExecutionPolicy, stage ModuleStage, stepID, laneID, moduleKey string, decision CheckpointDecision, codec artifactCodecEntry, artifacts []CheckpointArtifact) (CheckpointDecision, error) {
|
||||
type checkpointResolution struct {
|
||||
decision CheckpointDecision
|
||||
values []any
|
||||
artifacts []CheckpointArtifact
|
||||
}
|
||||
|
||||
func resolveCheckpointDecision(output *RunOutput, loader CheckpointLoader, policy CheckpointExecutionPolicy, stage ModuleStage, stepID, laneID, moduleKey string, decision CheckpointDecision, codec artifactCodecEntry, artifacts []CheckpointArtifact) (checkpointResolution, error) {
|
||||
decision = forceCheckpointDecision(policy, stepID, laneID, decision)
|
||||
resolution := checkpointResolution{decision: decision}
|
||||
if decision.Reused {
|
||||
resolution.values = make([]any, 0, len(artifacts))
|
||||
resolution.artifacts = make([]CheckpointArtifact, 0, len(artifacts))
|
||||
for _, artifact := range artifacts {
|
||||
if _, _, err := decodeCanonicalCheckpointArtifact(codec, artifact); err != nil {
|
||||
decision = checkpointDecision(CheckpointDecisionExecuted, checkpointArtifactReasonCode(err), "stored "+string(stage)+" artifact failed canonical codec validation")
|
||||
value, hydrated, err := decodeCanonicalCheckpointArtifact(codec, artifact)
|
||||
if err != nil {
|
||||
decision = checkpointDecision(CheckpointDecisionExecuted, checkpointArtifactReasonCode(err))
|
||||
resolution.values = nil
|
||||
resolution.artifacts = nil
|
||||
break
|
||||
}
|
||||
resolution.values = append(resolution.values, value)
|
||||
resolution.artifacts = append(resolution.artifacts, hydrated)
|
||||
}
|
||||
}
|
||||
resolution.decision = decision
|
||||
if output != nil {
|
||||
recordCheckpointEvent(output, loader, string(stage), stepID, laneID, moduleKey, decision)
|
||||
}
|
||||
if err := requireReusableCheckpoint(policy, stepID, laneID, decision); err != nil {
|
||||
return decision, err
|
||||
return resolution, err
|
||||
}
|
||||
return decision, nil
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
type SourceCheckpoint struct {
|
||||
@@ -296,19 +354,19 @@ func (noopCheckpointRecorder) NormalizeFailed(string, string, []CheckpointFinger
|
||||
|
||||
func (noopCheckpointLoader) Enabled() bool { return false }
|
||||
func (noopCheckpointLoader) Source(string) (SourceCheckpoint, CheckpointDecision) {
|
||||
return SourceCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled, "checkpoint loading disabled")
|
||||
return SourceCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
|
||||
}
|
||||
func (noopCheckpointLoader) Extract(string, string, []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision) {
|
||||
return ExtractCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled, "checkpoint loading disabled")
|
||||
return ExtractCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
|
||||
}
|
||||
func (noopCheckpointLoader) Merge(string, string, []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision) {
|
||||
return MergeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled, "checkpoint loading disabled")
|
||||
return MergeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
|
||||
}
|
||||
func (noopCheckpointLoader) Normalize(string, string, []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision) {
|
||||
return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled, "checkpoint loading disabled")
|
||||
return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
|
||||
}
|
||||
func (noopCheckpointLoader) AcceptedNormalize(string, string, string) (NormalizeCheckpoint, CheckpointDecision) {
|
||||
return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled, "checkpoint loading disabled")
|
||||
return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
|
||||
}
|
||||
|
||||
func checkpointExtractRunning(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) error {
|
||||
|
||||
Reference in New Issue
Block a user