executor¶
Package executor provides the execution engine for mooncake configuration steps.
Package executor implements the core execution engine for mooncake configuration plans.
The executor is responsible for: - Loading and validating configuration plans - Expanding steps (loops, includes, presets) - Evaluating conditions (when, unless, creates) - Dispatching actions to handlers - Managing execution context and variables - Tracking results and statistics - Emitting events for observability - Handling dry-run mode - Supporting privilege escalation (sudo/become)
# Architecture
The executor follows a pipeline architecture:
Each step goes through: 1. Pre-execution: Check when/unless/creates, apply tags filter 2. Variable processing: Merge step vars into context 3. Loop expansion: Expand with_items/with_filetree into multiple executions 4. Action execution: Dispatch to handler or legacy implementation 5. Post-execution: Evaluate changed_when/failed_when, register results 6. Event emission: Publish lifecycle events
# Execution Context
ExecutionContext carries all state needed during execution: - Variables: Step vars, global vars, facts, registered results - Template: Jinja2-like template renderer - Evaluator: Expression evaluator for conditions - Logger: Structured logging (TUI or text) - PathUtil: Path resolution and expansion - EventPublisher: Event emission for observability - Stats: Execution statistics (total, success, failed, changed, skipped)
# Action Dispatch
All 28 actions are registered in actions.Registry. Each action is dispatched by looking up its handler via actions.Get(actionType) and calling handler.Execute().
# Idempotency
The executor enforces idempotency through: - creates: Skip if path exists - unless: Skip if command succeeds - changed_when: Custom change detection - Handler implementations: Built-in state checking
# Dry-Run Mode
When DryRun is true: - No actual changes are made to the system - Handlers log what would happen - Template rendering still occurs (validates syntax) - File existence checks are performed (read-only) - Statistics track what would have changed
# Error Handling
Errors are wrapped with context using custom error types: - RenderError: Template rendering failures (field + cause) - EvaluationError: Expression evaluation failures (expression + cause) - CommandError: Command execution failures (command + exit code) - FileOperationError: File operation failures (path + operation + cause) - StepValidationError: Configuration validation failures - SetupError: Infrastructure/environment setup failures
Use errors.Is() and errors.As() for programmatic error inspection.
# Usage Example
// Load configuration
steps, err := config.ReadConfig("config.yml")
if err != nil {
return err
}
// Create executor
log := logger.NewTextLogger()
exec := NewExecutor(log)
// Execute with options
result, err := exec.Execute(config.Plan{Steps: steps}, ExecuteOptions{
DryRun: false,
Tags: []string{"setup", "deploy"},
Variables: map[string]interface{}{
"environment": "production",
},
})
// Check results
if !result.Success {
log.Errorf("Execution failed: %d failed steps", result.FailedSteps)
}
log.Infof("Summary: %d changed, %d unchanged, %d failed",
result.ChangedSteps, result.SuccessSteps-result.ChangedSteps, result.FailedSteps)
Index¶
- Constants
- Variables
- func AddGlobalVariables(scope *VariableScope)
- func DispatchStepAction(step config.Step, ec *ExecutionContext) error
- func ExecutePlan(ctx context.Context, p *plan.Plan, sudoPass string, mode actions.Mode, log logger.Logger, publisher events.Publisher) error
- func ExecutePlanWithCapture(ctx context.Context, p plan.Plan, sudoPass string, mode actions.Mode, log logger.Logger, publisher events.Publisher, capture RunCapture) error
- func ExecuteStep(step config.Step, ec *ExecutionContext) error
- func ExecuteSteps(steps []config.Step, ec *ExecutionContext) error
- func InspectPlan(p *plan.Plan, sudoPass string, log logger.Logger) ([]plan.StepInspection, error)
- func RegisterReverseDataType(name string, factory ReverseDataFactory)
- func Start(ctx context.Context, startConfig StartConfig, log logger.Logger, publisher events.Publisher) error
- type AssertionError
- func (e *AssertionError) Error() string
- func (e *AssertionError) Unwrap() error
- type CommandError
- func (e *CommandError) Error() string
- func (e *CommandError) Unwrap() error
- type DryRunLogger
- func NewDryRunLogger(log logger.Logger) *DryRunLogger
- func (d *DryRunLogger) LogArchiveExtraction(src, dest, format string, stripComponents int)
- func (d *DryRunLogger) LogAssertCheck(assertType, expected string)
- func (d *DryRunLogger) LogDirectoryCreate(path string, mode os.FileMode)
- func (d *DryRunLogger) LogDirectoryRemove(path string)
- func (d *DryRunLogger) LogFileCopy(src, dest string, mode os.FileMode, size int64)
- func (d *DryRunLogger) LogFileCopyNoChange(src, dest string)
- func (d *DryRunLogger) LogFileCreate(path string, mode os.FileMode, size int)
- func (d *DryRunLogger) LogFileDownload(url, dest string, mode os.FileMode)
- func (d *DryRunLogger) LogFileDownloadNoChange(_, dest string)
- func (d *DryRunLogger) LogFileRemove(path string, size int64)
- func (d *DryRunLogger) LogFileTouch(path string)
- func (d *DryRunLogger) LogFileUpdate(path string, mode os.FileMode, oldSize, newSize int)
- func (d *DryRunLogger) LogHardlinkCreate(src, dest string, force bool)
- func (d *DryRunLogger) LogHardlinkNoChange(src, dest string)
- func (d *DryRunLogger) LogPermissionsChange(path, mode, owner, group string, recurse bool)
- func (d *DryRunLogger) LogPermissionsNoChange(path string)
- func (d *DryRunLogger) LogPresetOperation(name string, paramsCount int)
- func (d *DryRunLogger) LogPrintMessage(message string)
- func (d *DryRunLogger) LogRegister(step config.Step)
- func (d DryRunLogger) LogServiceOperation(serviceName string, serviceAction config.ServiceAction, withSudo bool)
- func (d *DryRunLogger) LogShellExecution(command string, withSudo bool)
- func (d *DryRunLogger) LogSymlinkCreate(src, dest string, force bool)
- func (d *DryRunLogger) LogSymlinkNoChange(src, dest string)
- func (d *DryRunLogger) LogTemplateCreate(src, dest string, mode os.FileMode, size int)
- func (d *DryRunLogger) LogTemplateNoChange(src, dest string)
- func (d *DryRunLogger) LogTemplateRender(src, dest string, mode os.FileMode)
- func (d *DryRunLogger) LogTemplateUpdate(src, dest string, mode os.FileMode, oldSize, newSize int)
- func (d *DryRunLogger) LogVariableLoad(count int, source string)
- func (d *DryRunLogger) LogVariableSet(count int)
- type EvaluationError
- func (e *EvaluationError) Error() string
- func (e *EvaluationError) Unwrap() error
- type ExecutionContext
- func (ec *ExecutionContext) Clone() ExecutionContext
- func (ec *ExecutionContext) Ctx() context.Context
- func (ec *ExecutionContext) Effects() actions.Performer
- func (ec *ExecutionContext) EmitEvent(eventType events.Type, data interface{})
- func (ec *ExecutionContext) Evaluator() expression.Evaluator
- func (ec *ExecutionContext) EventPublisher() events.Publisher
- func (ec *ExecutionContext) Logger() logger.Logger
- func (ec *ExecutionContext) MergeUserVars(vars map[string]interface{})
- func (ec *ExecutionContext) Mode() Mode
- func (ec ExecutionContext) Privileged() security.Privileged
- func (ec ExecutionContext) RegisterResult(r Result, name string)
- func (ec *ExecutionContext) StepID() string
- func (ec *ExecutionContext) Template() template.Renderer
- func (ec *ExecutionContext) Variables() map[string]interface{}
- type ExecutionStats
- func NewExecutionStats() *ExecutionStats
- type FileOperationError
- func (e *FileOperationError) Error() string
- func (e *FileOperationError) Unwrap() error
- type LoopContext
- type Mode
- type Operation
- type RegisteredResult
- func (r RegisteredResult) ToMap() map[string]interface{}
- type RenderError
- func (e *RenderError) Error() string
- func (e *RenderError) Unwrap() error
- type Result
- func ChangedResult(op Operation, target string, data map[string]interface{}) *Result
- func FailedResult(op Operation, target string, err error, data map[string]interface{}) *Result
- func NewResult() *Result
- func NoopResult(target string, data map[string]interface{}) *Result
- func QueryResult(target string, data map[string]interface{}) *Result
- func (r *Result) MarshalJSON() ([]byte, error)
- func (r *Result) PublishObservation(env actions.ObserveResult, target string)
- func (r *Result) RegisterTo(variables map[string]interface{}, name string)
- func (r *Result) SetChanged(changed bool)
- func (r *Result) SetData(data map[string]interface{})
- func (r *Result) SetFailed(failed bool)
- func (r *Result) SetStderr(stderr string)
- func (r *Result) SetStdout(stdout string)
- func (r *Result) Status() string
- func (r *Result) ToMap() map[string]interface{}
- func (r *Result) ToRegisteredResult() RegisteredResult
- func (r *Result) UnmarshalJSON(b []byte) error
- type ReverseDataFactory
- type RunCapture
- func (c RunCapture) Plan() plan.Plan
- func (c *RunCapture) Steps() []StepRecord
- type RunServices
- type SetupError
- func (e *SetupError) Error() string
- func (e *SetupError) Unwrap() error
- type StartConfig
- type StepRecord
- type StepValidationError
- func (e *StepValidationError) Error() string
- type TxnCompletedChild
- type VariableScope
- func NewVariableScope() *VariableScope
- func (s VariableScope) Clone() VariableScope
- func (s *VariableScope) ToMap() map[string]interface{}
Constants¶
SkippedReason / CancelledReason enum string constants (proposal-02). Not Go enums (typed strings) on purpose — they're written to JSON and consumed by external tooling, and the existing fields are plain strings. Using bare consts keeps wire compatibility obvious.
const (
SkippedReasonWhen = "when"
SkippedReasonCreates = "creates"
SkippedReasonUnless = "unless"
SkippedReasonOnChange = "on_change"
SkippedReasonTagFilter = "tag_filter"
SkippedReasonTryAlreadyFailed = "try_already_failed"
CancelledReasonSigint = "sigint"
CancelledReasonFleetKill = "fleet_kill"
CancelledReasonMCPShutdown = "mcp_shutdown"
CancelledReasonTimeout = "timeout"
CancelledReasonCancelled = "cancelled"
)
Variables¶
Cancel-cause sentinels for context.WithCancelCause. Producers attach one of these as the cancel cause so syncResultEnvelope can classify the resulting Cancelled step accurately — distinguishing operator SIGINT from fleet-driven kill from MCP shutdown from a programmatic caller dropping its own ctx.
Today only ErrCancelSignal has a live producer (the SIGINT/SIGTERM handlers in cmd/kernel/apply.go and internal/fleet/orchestrator.go). ErrCancelFleet and ErrCancelMCP are declared so the eventual fleet-kill wire and MCP shutdown paths attach them without re-touching this package. Cancels without a registered cause map to CancelledReasonCancelled (the generic bucket).
var (
ErrCancelSignal = errors.New("mooncake: cancelled by signal")
ErrCancelFleet = errors.New("mooncake: cancelled by fleet kill")
ErrCancelMCP = errors.New("mooncake: cancelled by mcp shutdown")
)
func AddGlobalVariables¶
AddGlobalVariables populates scope.Facts, scope.Metrics, and scope.Env from the system. Facts (capabilities, configuration) come from facts.Collect; metrics (live CPU/GPU/memory/load/network) come from metrics.Collect with per-metric TTL caching; env is a snapshot of the parent process environment exposed to templates as `env.*` so users can reference `{{ env.HOME }}`, `{{ env.MY_API_KEY }}`, etc. Keys across facts and metrics are disjoint by contract — see metrics.disjoint_test.go.
func DispatchStepAction¶
DispatchStepAction executes the appropriate handler based on step type. All actions are now handled through the actions registry.
INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.
func ExecutePlan¶
func ExecutePlan(ctx context.Context, p *plan.Plan, sudoPass string, mode actions.Mode, log logger.Logger, publisher events.Publisher) error
ExecutePlan executes a pre-compiled plan. Emits events through the provided publisher for all execution progress.
Callers that need the typed *KernelResult substrate (R1.1b) should use ExecutePlanWithCapture or go through executor.Start with StartConfig.Capture set; this entry point does not surface the per-step records.
ctx is checked between steps — see Start for the cancellation contract.
func ExecutePlanWithCapture¶
func ExecutePlanWithCapture(ctx context.Context, p *plan.Plan, sudoPass string, mode actions.Mode, log logger.Logger, publisher events.Publisher, capture *RunCapture) error
ExecutePlanWithCapture runs a pre-compiled plan and fills the caller-supplied *RunCapture with the plan + per-step results, so the result can be lifted into an *apply.KernelResult.
This is the from-saved-plan analog of executor.Start with Capture set. Used by apply.NewRunnerFromPlan (R1.1c) so the saved-plan path produces the same typed kernel result as the compiled-from-config path. capture may be nil — equivalent to calling ExecutePlan directly.
ctx is checked between steps — see Start for the cancellation contract.
func ExecuteStep¶
ExecuteStep executes a single configuration step within the given execution context.
func ExecuteSteps¶
ExecuteSteps executes a sequence of configuration steps within the given execution context.
F016 stage-1(a): the loop checks ec.Svc.Ctx between steps and aborts with ctx.Err() if the context is cancelled. Handler-level cancellation (shell child interrupts, network step short-circuits) is the stage-3 audit and is not done here. nil ec.Svc.Ctx is treated as non-cancellable.
func InspectPlan¶
InspectPlan runs the plan in non-mutating ModePlan and returns per-step inspections. Each inspection reports whether applying the step would change state, the handler-supplied reason, and whether the step is checkable at all (shell steps return Checkable=false).
This is the primitive that powers `mooncake plan` after Spec 16: the plan command builds the static plan via planner.BuildPlan, then calls InspectPlan to fill in the per-step state predictions.
Implementation: subscribes a collector to a fresh SyncPublisher, dispatches the plan through the standard executor in check mode (which routes Runner handlers via dispatchRunner and legacy handlers via dispatchCheck — both emit EventStepChecked), then returns the collected results.
func RegisterReverseDataType¶
RegisterReverseDataType registers a factory for a concrete ReverseData payload type, keyed by the wire discriminator name. Convention: pass the Go type name (e.g. "FileReverseInfo") so reflect-based encode and registry-based decode agree on the discriminator without per-handler bookkeeping.
Panics on duplicate registration — silent overwrite would let a later handler shadow an earlier one and surface as a wire-decode drift far from the cause.
Called from each handler package's init() alongside actions.Register. The wire round-trip is the contract this registry implements: see Result.MarshalJSON / UnmarshalJSON (spec R2.1c phase 2).
func Start¶
func Start(ctx context.Context, startConfig StartConfig, log logger.Logger, publisher events.Publisher) error
Start begins execution of a mooncake configuration with the given settings. Always goes through the planner to expand loops, includes, and variables. Emits events through the provided publisher for all execution progress.
ctx is checked between steps in the step loop (F016 stage-1(a)). A cancelled ctx causes the run to return early with ctx.Err(); the in-flight step (if any) continues to completion — handler-level cancellation is the stage-3 audit. nil ctx is treated as context.Background() — non-cancellable, never aborts.
type AssertionError¶
AssertionError represents an assertion verification failure. Unlike other errors, assertions are expected to fail when conditions aren't met.
type AssertionError struct {
Type string // "command", "file", "http"
Expected string // What was expected
Actual string // What was found
Details string // Additional context (optional)
Cause error // Underlying error (optional)
}
func (*AssertionError) Error¶
func (*AssertionError) Unwrap¶
type CommandError¶
CommandError represents a command execution failure
type CommandError struct {
ExitCode int
Timeout bool
Duration string
Cause error // Optional underlying error (e.g., exec.ExitError, OS errors)
}
func (*CommandError) Error¶
func (*CommandError) Unwrap¶
type DryRunLogger¶
DryRunLogger provides consistent dry-run message formatting across all handlers.
INTERNAL: This type is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.
func NewDryRunLogger¶
NewDryRunLogger creates a dry-run logger wrapper.
INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.
func (*DryRunLogger) LogArchiveExtraction¶
LogArchiveExtraction logs a dry-run message for archive extraction.
func (*DryRunLogger) LogAssertCheck¶
LogAssertCheck logs a dry-run message for assertion verification.
func (*DryRunLogger) LogDirectoryCreate¶
LogDirectoryCreate logs a dry-run message for directory creation.
func (*DryRunLogger) LogDirectoryRemove¶
LogDirectoryRemove logs a dry-run message for directory removal.
func (*DryRunLogger) LogFileCopy¶
LogFileCopy logs a dry-run message for file copy.
func (*DryRunLogger) LogFileCopyNoChange¶
LogFileCopyNoChange logs a dry-run message when file copy is not needed.
func (*DryRunLogger) LogFileCreate¶
LogFileCreate logs a dry-run message for file creation.
func (*DryRunLogger) LogFileDownload¶
LogFileDownload logs a dry-run message for file download.
func (*DryRunLogger) LogFileDownloadNoChange¶
LogFileDownloadNoChange logs a dry-run message when file download is not needed.
func (*DryRunLogger) LogFileRemove¶
LogFileRemove logs a dry-run message for file removal.
func (*DryRunLogger) LogFileTouch¶
LogFileTouch logs a dry-run message for updating file timestamps.
func (*DryRunLogger) LogFileUpdate¶
LogFileUpdate logs a dry-run message for file update.
func (*DryRunLogger) LogHardlinkCreate¶
LogHardlinkCreate logs a dry-run message for hardlink creation.
func (*DryRunLogger) LogHardlinkNoChange¶
LogHardlinkNoChange logs a dry-run message when hardlink already exists correctly.
func (*DryRunLogger) LogPermissionsChange¶
LogPermissionsChange logs a dry-run message for permission changes.
func (*DryRunLogger) LogPermissionsNoChange¶
LogPermissionsNoChange logs a dry-run message when permissions are already correct.
func (*DryRunLogger) LogPresetOperation¶
LogPresetOperation logs a preset expansion operation in dry-run mode.
func (*DryRunLogger) LogPrintMessage¶
LogPrintMessage logs a print message in dry-run mode.
func (*DryRunLogger) LogRegister¶
LogRegister logs a dry-run message for registering results.
func (*DryRunLogger) LogServiceOperation¶
func (d *DryRunLogger) LogServiceOperation(serviceName string, serviceAction *config.ServiceAction, withSudo bool)
LogServiceOperation logs a dry-run message for service management.
func (*DryRunLogger) LogShellExecution¶
LogShellExecution logs a dry-run message for shell command execution.
func (*DryRunLogger) LogSymlinkCreate¶
LogSymlinkCreate logs a dry-run message for symlink creation.
func (*DryRunLogger) LogSymlinkNoChange¶
LogSymlinkNoChange logs a dry-run message when symlink already exists correctly.
func (*DryRunLogger) LogTemplateCreate¶
LogTemplateCreate logs a dry-run message for template creation.
func (*DryRunLogger) LogTemplateNoChange¶
LogTemplateNoChange logs a dry-run message when template produces no changes.
func (*DryRunLogger) LogTemplateRender¶
LogTemplateRender logs a dry-run message for template rendering.
func (*DryRunLogger) LogTemplateUpdate¶
LogTemplateUpdate logs a dry-run message for template update.
func (*DryRunLogger) LogVariableLoad¶
LogVariableLoad logs a dry-run message for loading variables.
func (*DryRunLogger) LogVariableSet¶
LogVariableSet logs a dry-run message for setting variables.
type EvaluationError¶
EvaluationError represents an expression evaluation failure
func (*EvaluationError) Error¶
func (*EvaluationError) Unwrap¶
type ExecutionContext¶
ExecutionContext holds per-scope state for a step sequence. Cloned when entering nested scopes (includes, loops); Svc is shared.
Field categories: - Svc: shared services and run configuration — pointer, never copied - Scope, CurrentDir, *: per-scope state — copied on Clone - CurrentStepID, CurrentResult: per-step state — not copied on Clone
type ExecutionContext struct {
// Svc holds all shared services and run-level configuration.
// All nested contexts share the same *RunServices pointer.
Svc *RunServices
// Scope holds all variable categories in typed sections.
// It is the authoritative source for variables.
// Use NewVariableScope() to create a ready scope.
Scope *VariableScope
// CurrentDir is the directory containing the current config file.
// Set per-file by the planner (each include / component flips it to
// the included file's own dir). All relative paths in step fields
// resolve against this Node-style: `./foo` is relative to the YAML
// file that declares the step.
CurrentDir string
// CurrentFile is the absolute path to the current config file being executed.
CurrentFile string
// Level tracks nesting depth for display indentation.
Level int
// CurrentIndex is the 0-based index of the current step within the current scope.
CurrentIndex int
// TotalSteps is the number of steps in the current execution scope.
TotalSteps int
// CurrentStepID is the unique identifier for the currently executing step.
// Not copied on Clone — resets per step.
CurrentStepID string
// CurrentResult holds the result of the currently executing step.
// Not copied on Clone — resets per step.
CurrentResult *Result
// CurrentAsUser is the step's declared AsUser, bound by
// dispatchRunner before calling runner.Run. Consumed by
// ec.Privileged() and ec.Effects() so handlers don't read
// step.AsUser for execution decisions — the primitive sees it
// transparently. Spec-72 Layer C.
//
// Not copied on Clone — each step gets its own binding; nested
// scopes (loops, includes) inherit through the per-step
// dispatchRunner re-binding rather than through structural
// copying. Empty for steps that didn't declare as_user.
CurrentAsUser string
// ChangedByStepID records the .Changed outcome of each step that has
// completed in this execution context, keyed by step.ID. Read by
// on_change child execution (spec-23 §1): a child runs iff
// ChangedByStepID[step.TriggeredBy] is true. Survives across steps so
// triggered children can look back at their parents; never copied to
// nested Clone() scopes (each scope tracks its own changes).
ChangedByStepID map[string]bool
// OpenTxns tracks per-transaction state for spec-30 transaction:
// blocks. Keyed by the transaction-parent step ID (which children
// carry as TxnParent). Created lazily when the first body child of
// a given TxnParent completes. The state type lives in
// internal/control — see kernel.md for the kernel-sub-system
// rationale (R0.1).
OpenTxns map[string]*control.TxnState
// CompletedByTxn tracks the in-order list of body children that
// ran to completion (no error) for each transaction, keyed by the
// transaction-parent step ID. Each entry carries the step and the
// concrete *Result the handler produced — the Reverser needs the
// Result to know what to undo. This slot lives in executor (not
// in control alongside TxnState) because *Result is an
// executor-package type and moving it would create a circular
// import.
CompletedByTxn map[string][]TxnCompletedChild
// OpenTries tracks per-try-block state for spec-23 §2 try / catch /
// finally. Keyed by the compound-parent step ID (which children
// carry as TryParent). Created lazily by the executor's trycatch.go
// when a try child's failure has to be recorded. The state type
// lives in internal/control.
OpenTries map[string]*control.TryState
}
func (*ExecutionContext) Clone¶
Clone creates a new ExecutionContext for a nested execution scope (include or loop). Svc is shared by pointer; Scope is deep-cloned (User+Results); per-step fields are reset.
func (*ExecutionContext) Ctx¶
Ctx returns the run-wide context (ec.Svc.Ctx). Handlers reach through this to plumb cancellation into shell-outs and HTTP calls so SIGINT / fleet kill / MCP shutdown propagates end-to-end (F2).
Returns context.Background() when Svc or Svc.Ctx is nil — production paths always populate both, but the guard keeps test-built contexts that skip RunServices construction from panicking. Returning a live (non-nil, non-cancellable) ctx is safer than nil for handlers that chain WithTimeout / WithCancel onto it.
func (*ExecutionContext) Effects¶
Effects returns a Performer pre-bound to the current step's AsUser. Like ec.Privileged(), the per-step binding means handlers don't have to thread step.AsUser through PerformerOpts — the Performer consults its bound state to decide sudo wrap and post-write chown.
func (*ExecutionContext) EmitEvent¶
EmitEvent publishes an event to all subscribers
func (*ExecutionContext) Evaluator¶
Evaluator returns the expression evaluator.
func (*ExecutionContext) EventPublisher¶
EventPublisher returns the event publisher.
func (*ExecutionContext) Logger¶
Logger returns the logger.
func (*ExecutionContext) MergeUserVars¶
MergeUserVars merges the provided key-value pairs into the user variable scope. Logs a warning for any key that shadows a system fact or metric.
Drops the `if ec.Svc != nil` guard the pre-cleanup version carried — every other accessor on ExecutionContext (EmitEvent, Mode, Effects, Privileged, Template / Evaluator / Logger / EventPublisher) derefs ec.Svc unconditionally. Svc is always non-nil in production paths (Start / executePlanWithCapture sets it on every constructed context); a future test that builds an EC without Svc panics here exactly the same way it would in any of the peer accessors. Convention drift closed.
func (*ExecutionContext) Mode¶
Mode returns the current dispatch mode (ModeApply or ModePlan).
func (*ExecutionContext) Privileged¶
Privileged returns the spec-72 Layer C escalation primitive, pre-bound to the current step's AsUser. Handlers should call ctx.Privileged().Run(...) / .Command(...) for shell-outs and let the primitive decide the sudo wrap from the bound AsUser. No per-call `become bool` plumbing; no per-handler `step.ShouldBecome` reads. dispatchRunner sets ec.CurrentAsUser from step.AsUser before calling Run, so each step sees a primitive bound to its own declared identity.
func (*ExecutionContext) RegisterResult¶
RegisterResult registers a Result under the given name for use in subsequent steps.
func (*ExecutionContext) StepID¶
StepID returns the current step ID.
func (*ExecutionContext) Template¶
Template returns the template renderer.
func (*ExecutionContext) Variables¶
Variables returns all variables merged into a flat map for template/expression engines.
type ExecutionStats¶
ExecutionStats holds shared statistics counters for execution tracking. All fields are pointers to enable shared state across nested execution contexts.
type ExecutionStats struct {
// Global tracks total non-skipped steps across the entire execution tree
Global *int
// Executed counts successfully completed steps
Executed *int
// Skipped counts steps skipped due to when conditions or tag filtering
Skipped *int
// Failed counts steps that failed with errors
Failed *int
// Changed counts steps that resulted in a system change
Changed *int
// OK counts steps that completed successfully without changing the
// system (F6 / proposal-02). Mutually exclusive with Changed at each
// step decision site — a step is either "ran and did nothing" (OK)
// or "ran and mutated" (Changed). Invariant: OK + Changed == Executed
// for every successful step. Lifts the renderer-side derivation
// (`ok := SuccessSteps - ChangedSteps`) onto a first-class field so
// downstream consumers (agentd, MCP, SDK) read it directly.
OK *int
// Reverted counts steps whose changes were undone by a transaction's
// LIFO Reverse() pass (MT-45). Reverted steps are subtracted from
// Changed at rollback time so the recap reflects net effect, not
// gross writes-then-undos.
Reverted *int
// Cancelled counts steps interrupted mid-execution per proposal-02
// (SIGINT, fleet kill, timeout). Distinct from Failed: a cancelled
// step didn't fail on its own merits; the exit-code aggregator
// maps cancelled>0 to 130, failed>0 to 1.
Cancelled *int
// Healed counts assert steps that failed on first dispatch, ran
// their declared heal: child plan, and passed the re-check
// (proposal-11). Distinct from Failed (assert never passed) and
// Changed (heal children's own changes are counted separately).
// A non-zero Healed means the system drifted and was restored
// in-band — the kernel-level self-healing signal.
Healed *int
}
func NewExecutionStats¶
NewExecutionStats creates a new ExecutionStats with all counters initialized to zero
type FileOperationError¶
FileOperationError represents a file operation failure
type FileOperationError struct {
Operation string // "create", "read", "write", "delete", "chmod", "chown", "link"
Path string
Cause error
}
func (*FileOperationError) Error¶
func (*FileOperationError) Unwrap¶
type LoopContext¶
LoopContext holds the current loop iteration state for a step executing inside a with_items or with_filetree loop. It is stored in VariableScope.Loop so ToMap() can inject item/index/first/last without polluting the User map.
type Mode¶
Mode and its constants live in the actions package; re-exported here for backward source compatibility during the Spec 16 migration.
type Operation¶
Operation is the proposal-01 result-envelope verb describing what the step did to its target. Handlers should pick the value that best fits the action's lifecycle. Empty string is "unspecified" — legacy handlers that haven't migrated yet.
const (
OpCreate Operation = "create"
OpUpdate Operation = "update"
OpDelete Operation = "delete"
OpNoop Operation = "noop"
OpQuery Operation = "query"
OpReverted Operation = "reverted"
)
type RegisteredResult¶
RegisteredResult is a snapshot of a Result stored in VariableScope.Results. It is a flat copy — no pointer aliasing — so the scope can be safely cloned.
type RegisteredResult struct {
Stdout string
Stderr string
Rc int
Failed bool
Changed bool
Skipped bool
Cancelled bool
Operation Operation
Target string
Error string
SkippedReason string
CancelledReason string
DurationMs int64
Reason string
Data map[string]interface{}
}
func (RegisteredResult) ToMap¶
ToMap converts a RegisteredResult to map[string]interface{} for template engines.
Mirrors Result.ToMap — proposal-01 envelope at the top level, action payload nested under `data`.
type RenderError¶
RenderError represents a template rendering failure
func (*RenderError) Error¶
func (*RenderError) Unwrap¶
type Result¶
Result represents the outcome of executing a step and can be registered to variables for use in subsequent steps via the "register" field.
Field usage varies by step type:
Shell steps: - Stdout: captured standard output from the command - Stderr: captured standard error from the command - Rc: exit code (0 for success, non-zero for failure) - Failed: true if Rc != 0 - Changed: always true (commands are assumed to make changes)
File steps (file with state: file or directory): - Rc: 0 for success, 1 for failure - Failed: true if file/directory operation failed - Changed: true if file/directory was created or content modified
Template steps: - Rc: 0 for success, 1 for failure - Failed: true if template rendering or file write failed - Changed: true if output file was created or content changed
Variable steps (vars, include_vars): - All fields remain at default values (not currently used)
The Skipped field is reserved for future use but not currently set by any step type.
type Result struct {
// Stdout contains the standard output from shell commands.
// Only populated by shell steps.
Stdout string `json:"stdout"`
// Stderr contains the standard error from shell commands.
// Only populated by shell steps.
Stderr string `json:"stderr"`
// Rc is the return/exit code.
// For shell steps: the command's exit code (0 = success).
// For file/template steps: 0 for success, 1 for failure.
Rc int `json:"rc"`
// Failed indicates whether the step execution failed.
// Set to true when shell commands exit non-zero or when file/template operations error.
//
// Proposal-06 contract: Failed=false REQUIRES Error="". A query-style
// observation that returns "absent" is success, not failure — populate
// Data with {found: false, ...} and leave Error empty.
Failed bool `json:"failed"`
// Changed indicates whether the step made modifications to the system.
// Shell steps: always true (commands assumed to make changes).
// File steps: true if file/directory was created or modified.
// Template steps: true if output file was created or content changed.
Changed bool `json:"changed"`
// Operation is the proposal-01 result-envelope verb: a one-word
// taxonomy of what this step did. Values: create, update, delete,
// noop, query, reverted. Empty string is "unspecified" (legacy
// handlers); new code should populate it. The recap counter
// (proposal-02) reads this to decide which bucket the step lands in
// alongside Failed / Skipped / Cancelled.
Operation Operation `json:"operation,omitempty"`
// Target is the primary thing the action acted on — the "X" in "did
// Y to X" (path, package name, peer address, etc.). Per proposal-01,
// promoted out of the per-handler Data bag so typed-diff consumers
// and the recap line can read it uniformly.
Target string `json:"target,omitempty"`
// Error carries a single-string diagnostic when Failed is true. Per
// proposal-06 this field MUST be empty when Failed is false — the
// pre-proposal pattern of `failed: false, error: "no matching X"`
// (observe.process, wait.http, os.mount, os.firewall) is the bug
// this contract closes.
Error string `json:"error,omitempty"`
// Cancelled marks the step as interrupted mid-execution (SIGINT,
// fleet kill, timeout). Distinct from Failed — a cancelled step
// didn't fail on its own merits. Recap proposal-02 has its own
// `cancelled` bucket; exit code aggregation maps cancelled>0 to 130.
Cancelled bool `json:"cancelled,omitempty"`
// SkippedReason explains why a Skipped step did not run. Enum:
// when, creates, unless, on_change, tag_filter, try_already_failed.
// Required when Skipped is true.
SkippedReason string `json:"skipped_reason,omitempty"`
// CancelledReason explains how a Cancelled step was interrupted.
// Enum: sigint, fleet_kill, timeout. Required when Cancelled is true.
CancelledReason string `json:"cancelled_reason,omitempty"`
// WouldChange indicates that a plan-mode (non-mutating) inspection
// predicts the step would change the system if executed.
// Set by handlers running in ModePlan (Spec 16). Mirrors today's
// CheckResult.WouldChange and is the eventual replacement.
WouldChange bool `json:"would_change,omitempty"`
// Reason is a short human-readable description of the result, e.g.
// "would create directory", "already matches", "content differs".
// Populated alongside WouldChange and Changed.
Reason string `json:"reason,omitempty"`
// Checkable indicates whether the action supports plan-mode inspection.
// False for actions like shell where no prediction is possible (the
// command must run to know its effect). Set by handlers in ModePlan.
Checkable bool `json:"checkable,omitempty"`
// Skipped is reserved for future use to indicate skipped steps.
// Currently not set by any step type.
Skipped bool `json:"skipped"`
// Data holds custom result data set by actions via SetData.
// This allows actions to provide additional structured information
// that can be accessed in templates and registered results.
Data map[string]interface{} `json:"data,omitempty"`
// Detail holds action-specific plan-mode data (e.g. effects.ContentDiff).
// Only populated in ModePlan. Not serialised into registered results.
Detail any `json:"-"`
// AppliedDiff is the typed actions.Diff captured at apply time for
// handlers that implement actions.Differ. The diff is computed
// just before runner.Run executes, so it represents what the
// handler intends to change (Before / After of the predicted
// mutation). nil for handlers that don't implement Differ or for
// plan-mode (where the diff already flows through the StepChecked
// event). Lives on Result so the apply.captureSubscriber can
// thread it into the spec-68 runlog StepEntry — typed Diff per
// step lets `mooncake explain r/<id>` and `explain <resource>`
// show what each step did, not just that it ran.
//
// Kept as `any` (rather than *actions.Diff) to keep the executor
// package free of an actions.Differ-payload type dependency; the
// apply layer narrows it when projecting to runlog.
AppliedDiff any `json:"-"`
// ReverseData holds action-specific apply-time state captured for
// later use by the handler's Reverse() method (spec-22 phase 5).
// Typically populated in ModeApply BEFORE the mutation runs so
// post-apply Reverse can construct an inverse step from the
// pre-state. Parallel to Detail rather than overloading it —
// Detail is plan-time output (ContentDiff for `--diff`),
// ReverseData is apply-time capture, distinct lifetimes and
// consumers.
//
// Wire encoding (R2.1c phase 2): the field is serialised through
// a discriminator envelope (`{"type": "<go-type-name>", "data":
// {...}}`) so it round-trips through the agentd /v1/runs/{id}/
// result endpoint and fleet-wide Reverse() can compose against
// the captured pre-state of each peer. The default `json:"-"` is
// gone — Result.MarshalJSON / UnmarshalJSON handle the envelope
// explicitly via the registry in reverse_registry.go.
ReverseData any `json:"-"`
// Timing information
StartTime time.Time `json:"start_time,omitempty"`
EndTime time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration_ms,omitempty"` // Duration in time.Duration format
}
func ChangedResult¶
ChangedResult builds a successful mutation result. Op must be one of OpCreate, OpUpdate, or OpDelete. Changed=true.
func FailedResult¶
FailedResult builds a mutation-failed result (proposal-06: mutation that didn't happen IS failure). Op is the operation that was attempted; data may carry partial state captured before the failure. Failed=true, Rc=1, Error=err.Error().
func NewResult¶
NewResult creates a new Result with default values.
func NoopResult¶
NoopResult builds an idempotent "already at target state" result. Changed=false, Operation=OpNoop.
func QueryResult¶
QueryResult builds a read-only observation result (observe.*, read.*, repo.search/tree, wait.* on success). Changed=false, Failed=false, Error="" — per proposal-06, "absent" / "not matching" is success.
func (*Result) MarshalJSON¶
MarshalJSON serialises Result with ReverseData wrapped in a discriminator envelope when populated. The discriminator is the payload's Go type name (e.g. "FileReverseInfo"); the receiver must be a pointer to a registered type so the decode side can look it up.
When ReverseData is nil, the output matches the pre-phase-2 shape — no `reverse_data` key at all. Old daemons / clients that don't know about the field are unaffected on the read side, and new code that consumes them sees `nil` ReverseData (which the existing "ReverseData is nil" refusal in handlers' Reverse() already handles).
func (*Result) PublishObservation¶
PublishObservation lands a spec-59 ObserveResult onto this Result using the proposal-01 / proposal-06 envelope conventions:
- Operation = "query"
- Target = the caller-supplied selector ("name=foo", "host:port", …)
- Data carries the typed payload (found / value / as_of)
- Probe-side failures (env.Error != "") promote to envelope
Failed=true + Error=env.Error so the recap counts the step
correctly and consumers don't have to dig into Data["error"]
to know the observation broke.
Plan-mode handlers should leave env.Error empty; the handler's Reason field is the right place for the "deferred to apply" message.
func (*Result) RegisterTo¶
RegisterTo registers this result to the variables map under the given name. The result can be accessed using nested field syntax (e.g., "result.stdout", "result.rc") in templates and when conditions.
func (*Result) SetChanged¶
SetChanged marks whether the action made changes.
func (*Result) SetData¶
SetData sets custom result data. This merges the provided data into the result's ToMap output, allowing actions to provide additional structured information.
func (*Result) SetFailed¶
SetFailed marks the result as failed.
func (*Result) SetStderr¶
SetStderr sets the stderr output.
func (*Result) SetStdout¶
SetStdout sets the stdout output.
func (*Result) Status¶
Status returns a string representation of the result status.
Precedence (proposal-02): failed > cancelled > skipped > reverted > changed > ok. Cancelled and reverted are new buckets the recap counter cares about; status mirrors the same precedence so the per-step text marker is consistent with the headline.
func (*Result) ToMap¶
ToMap converts Result to a map for use in template variables.
Proposal-01 envelope: action-specific payload stays NESTED under `data` rather than being flattened into the top-level map. So `register: r` + `{{ r.data.cores }}` is the access path for typed fields; cross-cutting envelope keys (changed, failed, operation, target, error, stdout, stderr, rc, status, duration_ms, reason) live at the top level.
"reason" is included so step.completed consumers (notably the pilot loop's stdoutCapture, which builds per-step summaries fed back to the LLM) can see the handler's own one-liner without reaching into the executor.Result struct. Handlers that leave Reason empty get an empty string here — pilot's summarizer falls back to action+status.
func (*Result) ToRegisteredResult¶
ToRegisteredResult converts a *Result into a RegisteredResult snapshot.
func (*Result) UnmarshalJSON¶
UnmarshalJSON deserialises Result, looking up the ReverseData payload type via the registry and materialising the concrete type when known. Unknown discriminators decode to nil ReverseData — forward compatibility for newer daemons whose handler types this binary doesn't know about. Returns an error only when the envelope itself is malformed or the concrete type's Unmarshal fails (the latter signals real corruption, not unknown types).
type ReverseDataFactory¶
ReverseDataFactory produces a fresh zero value of a typed ReverseData payload (e.g. `func() any { return &FileReverseInfo{} }`). Callers register one factory per concrete type at init() time; Result.UnmarshalJSON looks it up by discriminator to materialise the concrete type from the wire envelope.
type RunCapture¶
RunCapture is an optional sink the executor populates during Start / ExecutePlan with the compiled plan and per-step outcomes. Built specifically for R1.1b's typed *KernelResult on internal/apply.Runner: the Runner installs a *RunCapture before Start, reads its contents after Start returns, and converts them into the kernel-surface KernelResult.
Other callers of executor.Start pass nil — the executor's hot path is a no-op when Capture is unset.
Concurrency: protected by an internal mutex. The executor runs steps sequentially today, but the mutex guards against future parallel execution and against concurrent reads by the holder of the pointer.
func (*RunCapture) Plan¶
Plan returns the compiled plan recorded during the run, or nil if the run never reached plan compilation.
func (*RunCapture) Steps¶
Steps returns a snapshot of the per-step records in execution order. The returned slice is owned by the caller; subsequent appends to the capture will not affect it.
type RunServices¶
RunServices holds the shared, immutable-after-construction services and configuration for a mooncake run. One instance is created per run and referenced by all nested ExecutionContexts via pointer.
type RunServices struct {
Template template.Renderer
Evaluator expression.Evaluator
PathUtil *pathutil.PathExpander
FileTree *filetree.Walker
Redactor *security.Redactor
EventPublisher events.Publisher
Logger logger.Logger
Stats *ExecutionStats
Mode actions.Mode
Tags []string
SudoPass string
// PasswordlessSudo is set at run construction by probing
// `sudo -n true`. When true, Sudo+AsUser steps don't need a
// password flag — preflight passes and BecomeRunner uses
// `sudo -n <cmd>` instead of `sudo -S`.
//
// spec-72 phase 1: derived from Escalation.Reason ==
// EscalationAvailablePasswordless. Kept on RunServices as the
// backward-compat shim while preflight/BecomeRunner/effects still
// read the bool; phase 5 drops it in favor of Escalation.
PasswordlessSudo bool
// Escalation is the unified, once-per-run answer to "can this
// process escalate to root, and if not, why not?". Populated by
// security.ProbeEscalation at RunServices construction
// (spec-72 §1). Consumed by *security.Privileged for the actual
// sudo wrap and by preflight for diagnostic messages.
Escalation security.EscalationReport
// Capture, if non-nil, records the compiled plan and per-step
// outcomes for callers that want the typed *KernelResult shape
// (internal/apply.Runner for R1.1b). nil for the legacy
// executor.Start callers that only care about the error return.
Capture *RunCapture
// Ctx carries cancellation/deadline state for the run. The step
// loop in ExecuteSteps checks Ctx.Err() before dispatching each
// step and aborts cleanly if the context is cancelled (F016
// stage-1(a) — handler-level cancellation is the stage-3 audit).
// May be nil in test contexts that construct RunServices directly;
// callers that want cancellation set this to a context that the
// embedding shell (daemon Shutdown / CLI signal handler) cancels.
Ctx context.Context
// Modules is the playbook's `modules:` alias map (spec-67). Read by the
// `use:` action handler so alias references like `use: postgres` resolve
// to a cached module. Empty when the playbook declares no modules.
Modules map[string]string
}
type SetupError¶
SetupError represents infrastructure or configuration setup failures
type SetupError struct {
Component string // "become", "timeout", "sudo", "user", "group"
Issue string // What went wrong
Cause error // Underlying error (optional)
}
func (*SetupError) Error¶
func (*SetupError) Unwrap¶
type StartConfig¶
StartConfig contains configuration for starting a mooncake execution.
type StartConfig struct {
ConfigFilePath string
// VarsFilePaths are loaded in order; later files override earlier on key
// collision. Mirrors how `mooncake apply -v a.yml -v b.yml` reads on
// the CLI and how `vars_files: ["a.yml", "b.yml"]` works in the agentd
// /v1/runs payload.
VarsFilePaths []string
SudoPass string // Sudo password provided directly (use SudoPassFile for better security)
SudoPassFile string
AskBecomePass bool
InsecureSudoPass bool
Tags []string
// SkipTags excludes any step whose tags intersect this list
// (MT-58, `--skip-tags`). Composes with Tags: a step runs only
// when Tags admits it AND SkipTags doesn't exclude it.
SkipTags []string
// Names is the spec-50 step-name filter (`--step-filter name=<x>`).
// AND'd with Tags at plan-build time: a step must pass both.
Names []string
// Artifact configuration
ArtifactsDir string
CaptureFullOutput bool
MaxOutputBytes int
MaxOutputLines int
// Capture, if non-nil, is populated by Start with the compiled
// plan and per-step records. R1.1b's internal/apply.Runner uses
// this to build its typed *KernelResult. Other callers leave nil.
Capture *RunCapture
}
type StepRecord¶
StepRecord is a single per-step entry — the typed step the executor dispatched plus the executor.Result it produced. apply.StepResult is the public mirror.
type StepRecord struct {
// Step is the typed step as it ran (after planner expansion). Carries
// the action, ID, tags, transaction membership, and — critically —
// the ReverseData snapshot captured pre-mutation (spec-22 phase 5).
Step config.Step
// Result is the executor.Result produced by the handler.
// nil only if the step was filtered out at dispatch time before a
// result could be assembled.
Result *Result
// Reverted is set true when a transaction rollback's Reverse()
// pass undid this step's mutation. F054 / spec-30: rolled-back
// steps stay in the record (the original action ran; the
// inverse undid it) so `mooncake history` and `explain` can
// show "this step ran but was rolled back". Mutated via
// markStepReverted from transaction.go's rollback walk.
Reverted bool
}
type StepValidationError¶
StepValidationError represents step parameter validation failure during execution
func (*StepValidationError) Error¶
type TxnCompletedChild¶
TxnCompletedChild captures one body child's step + result for later Reverse() consumption. Stored in ExecutionContext.CompletedByTxn — the *Result field keeps this type out of internal/control.
type VariableScope¶
VariableScope holds all variables available to a step, each in its native type. ToMap() merges them at the template/expression engine boundary.
type VariableScope struct {
// User holds vars from Vars actions and config vars: blocks.
User map[string]interface{}
// Facts holds system facts (os, arch, hostname, cpu_cores, etc.)
// Set once at run start, read-only during execution.
Facts *facts.Facts
// Loop holds the current loop iteration state, or nil outside a loop.
Loop *LoopContext
// Results holds registered results keyed by step.As name.
Results map[string]RegisteredResult
// ResultOrigins tracks the writer for each Results entry, keyed by the
// same `as:` name. Used by the spec-37 collision check to identify
// for_each-sibling overwrites and skip the warning in that case.
// Kept in lockstep with Results — clones, fresh-scope, and writes all
// update both maps together.
ResultOrigins map[string]resultOrigin
// Metrics holds live daemon metrics (cpu_usage_pct, memory_used_pct, etc.)
// Set once at run start, read-only during execution.
Metrics *metrics.Metrics
// Env holds the parent-process environment as a string→string map.
// Exposed to templates as `env.*` so users can write
// `{{ env.HOME }}` or `{{ env.MY_API_KEY }}`. Set once at run start
// by AddGlobalVariables; read-only during execution. Secrets-in-env
// flow this way too — auditors should treat env-driven values the
// same way as user vars, not as system facts.
Env map[string]string
// ApplyStartedAt is the wall-clock time the run's scope was
// initialized — set once by AddGlobalVariables and held constant
// for every step that follows. Exposed to templates as
// `apply_started_at` so playbooks can build rolling-backup paths
// like `{{ apply_started_at | strftime:"%Y%m%d_%H%M%S" }}` where
// every step in one apply shares the same suffix (the
// shell:`$(date)` form can mismatch across step boundaries).
// Zero value means "scope wasn't initialized by AddGlobalVariables"
// — ToMap omits the key in that case so test fixtures stay clean.
ApplyStartedAt time.Time
}
func NewVariableScope¶
NewVariableScope returns an empty scope ready for use.
func (*VariableScope) Clone¶
Clone deep-copies User and Results; shares Facts and Metrics pointers (read-only after init). Loop is intentionally NOT copied — it is per-step state.
func (*VariableScope) ToMap¶
ToMap merges all sections into map[string]interface{} for the template/expression engine. Priority (higher overwrites lower): Facts \< Metrics \< Env \< User \< Results \< Loop. Env sits below User so a config-level `vars: { env: ... }` can shadow the namespace if a playbook needs to (matches the same shadowing freedom user vars already have over facts/metrics).
Generated by gomarkdoc