autofix

package
v0.31.52 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package autofix holds the whitelist of corrective actions pulse may invoke without further human approval. Every autofix must be:

  • idempotent: running it twice in a row produces the same end state;
  • scoped: touches only files pulse understands to be its own;
  • bounded: completes in under a minute on typical workspaces;
  • safe-on-failure: partial failures leave the workspace in a consistent state (no half-deleted, half-renamed, half-compressed files).

New autofixes are added by implementing the Fixer interface and registering a constructor in the DefaultRegistry initializer below. The decider cannot invoke an autofix whose name is not in both the code registry AND the operator's config allow-list — this double gate is intentional.

Index

Constants

View Source
const CleanupStaleTmpName = "cleanup_stale_tmp"

CleanupStaleTmpName is the canonical name used in configs and logs.

View Source
const CompressOldLogsName = "compress_old_logs"

CompressOldLogsName is the canonical name used in configs and logs.

Variables

This section is empty.

Functions

This section is empty.

Types

type CleanupStaleTmp

type CleanupStaleTmp struct {
	// Dirs is the list of absolute tmp paths to scan. Non-existent
	// entries are silently skipped. Typical values:
	//   <workspace>/tmp
	//   <workspace>/_shared/tmp
	Dirs []string
	// MaxAge is the minimum file age required before deletion. Zero
	// falls back to 7 days, matching the Phase 1 default.
	MaxAge time.Duration
	// Now is injectable for tests; real code uses time.Now.
	Now func() time.Time
}

CleanupStaleTmp removes plain files older than MaxAge from the workspace's temporary directories. It intentionally DOES NOT:

  • follow symlinks (they are skipped; pulse must never chase links out of workspace-controlled paths);
  • delete directories (even empty ones — directory cleanup is a separate concern and can remove user state accidentally);
  • descend into subdirectories (recursion widens the blast radius for a background agent; tmp layouts are expected to be flat);

The fixer is idempotent: a second run in quick succession finds no candidates and reports Changed=false.

func (*CleanupStaleTmp) Name

func (c *CleanupStaleTmp) Name() string

func (*CleanupStaleTmp) Run

func (c *CleanupStaleTmp) Run(ctx context.Context) (Result, error)

type CompressOldLogs

type CompressOldLogs struct {
	// LogsDir is an absolute path to the directory being scanned. It is
	// usually <workspace>/logs. Non-existent dirs are treated as empty.
	LogsDir string
	// MaxAge is the minimum file age required before compression. Zero
	// falls back to 7 days, matching the Phase 1 default.
	MaxAge time.Duration
	// Now is injectable for tests; real code uses time.Now.
	Now func() time.Time
}

CompressOldLogs gzips .log files older than MaxAge found under the workspace logs directory. It is designed to run under the pulse watchdog as a periodic housekeeping fix, so its touch is narrow:

  • only files ending in ".log" (not ".log.gz", not other extensions)
  • only files with modification time older than MaxAge
  • compressed output is written alongside as "<file>.log.gz"
  • original is removed only after successful compression and fsync

The fixer is idempotent: a second run finds no candidates (they are already gzipped) and reports Changed=false.

func (*CompressOldLogs) Name

func (c *CompressOldLogs) Name() string

func (*CompressOldLogs) Run

func (c *CompressOldLogs) Run(ctx context.Context) (Result, error)

type ErrUnknown

type ErrUnknown struct{ Name string }

ErrUnknown is returned when a requested autofix is not registered.

func (ErrUnknown) Error

func (e ErrUnknown) Error() string

type Fixer

type Fixer interface {
	Name() string
	Run(ctx context.Context) (Result, error)
}

Fixer is the single-method interface each autofix implementation must satisfy. Fixers are expected to be pure functions of their constructor arguments (workspace dir, clock, etc.) — no package-level state.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a concurrent-safe name → Fixer map. Its primary use is the pulse runtime looking up a named autofix from a Decision.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty registry.

func (*Registry) AllowedIntersection

func (r *Registry) AllowedIntersection(configured []string) []string

AllowedIntersection returns the intersection of the registry's names and the operator-configured allow-list. This is the exact list that should be handed to the decider as the autofix policy — anything outside this set cannot safely run.

func (*Registry) Has

func (r *Registry) Has(name string) bool

Has reports whether a fixer with the given name is registered.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns all registered fixer names in sorted order. Used by pulse startup logging and by HTTP handlers that want to report which autofixes are known.

func (*Registry) Register

func (r *Registry) Register(f Fixer)

Register adds a fixer to the registry. Re-registering a name replaces the previous fixer — callers that care should check Has first.

func (*Registry) Run

func (r *Registry) Run(ctx context.Context, name string) (Result, error)

Run executes the named fixer. It returns ErrUnknown when the name is not registered, letting callers distinguish that from legitimate fixer errors.

type Result

type Result struct {
	Name    string         `json:"name"`
	Summary string         `json:"summary,omitempty"`
	Details map[string]any `json:"details,omitempty"`
	Changed bool           `json:"changed"`
}

Result captures what an autofix did. Fields are optional; an empty Result is valid for autofixes that have nothing meaningful to report (e.g. "there was nothing to clean up").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL