kit

package
v0.2026203.1311 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Overview

Package kit is the importable contract a HOST-COUPLED plugin candy implements to run against charly's live check engine — the seam that lets a check verb whose logic needs the running deployment (exec-in-container, host TCP dial, host-vantage HTTP) live in its own candy module instead of charly's module.

A host-coupled verb candy implements CheckVerbProvider; charly runs it in EITHER placement, invisibly above the registry: IN-PROCESS (compiled-in — charly passes the live *Runner as a CheckContext) OR OUT-OF-PROCESS (the CheckContext legs are served back to the candy over the host's reverse channel — ExecutorService for Exec + CheckContextService for HTTPDo/AddBackground, F2 — and the scalar legs ride the env_json snapshot). RunVerb is identical in both. This package imports only the stdlib + charly/spec (the generated param/Op types), so a candy module can import it without pulling charly's package main.

Index

Constants

View Source
const (
	CheckLevelNone    = "none"
	CheckLevelBuild   = "build"
	CheckLevelNoAgent = "noagent"
	CheckLevelAgent   = "agent"
)

checklevel.go — the per-box acceptance-depth ladder.

CheckLevel controls how deep `charly check run <bed>` drives a box's acceptance, gated by the do:/context: axes of its Op steps:

none    — skip acceptance entirely
build   — build-context ops only (charly check box)
noagent — build + deploy + runtime act/assert, NO do: instruct (default)
agent   — also run do: instruct steps through the agent grader

Authored as BoxConfig.CheckLevel; baked into the ai.opencharly.check_level capability label so the bed runner reads the rung from the built image without the source repo. Lives in kit so a plugin candy that drives acceptance shares the one ladder.

View Source
const (
	ScopeSystem      = spec.ScopeSystem
	ScopeUser        = spec.ScopeUser
	ScopeUserProfile = spec.ScopeUserProfile

	ReverseOpPackageRemove  = spec.ReverseOpPackageRemove
	ReverseOpCargoUninstall = spec.ReverseOpCargoUninstall
	ReverseOpNpmUninstallG  = spec.ReverseOpNpmUninstallG
	ReverseOpPixiEnvRemove  = spec.ReverseOpPixiEnvRemove
	ReverseOpRmFileSystem   = spec.ReverseOpRmFileSystem
	ReverseOpRmFileUser     = spec.ReverseOpRmFileUser
	ReverseOpRmDirRecursive = spec.ReverseOpRmDirRecursive
	ReverseOpServiceDisable = spec.ReverseOpServiceDisable
	ReverseOpServiceRemove  = spec.ReverseOpServiceRemove
	ReverseOpRemoveDropin   = spec.ReverseOpRemoveDropin
	ReverseOpRestoreEnabled = spec.ReverseOpRestoreEnabled
	ReverseOpRemoveManaged  = spec.ReverseOpRemoveManaged
	ReverseOpRemoveEnvdFile = spec.ReverseOpRemoveEnvdFile
	ReverseOpRemoveRepoFile = spec.ReverseOpRemoveRepoFile
	ReverseOpCoprDisable    = spec.ReverseOpCoprDisable
	ReverseOpPluginScript   = spec.ReverseOpPluginScript
)
View Source
const (
	StatusPass = spec.StatusPass
	StatusFail = spec.StatusFail
	StatusSkip = spec.StatusSkip
)
View Source
const (
	// DocShapeEmpty — a scalar-null / empty mapping document (nothing to load).
	DocShapeEmpty = spec.DocShapeEmpty
	// DocShapeNode — the unified name-first node-form. See spec.DocShapeNode.
	DocShapeNode = spec.DocShapeNode
)
View Source
const (
	UnifiedFileName = "charly.yml" // the ONE box/candy manifest filename
	DefaultBoxDir   = "box"        // discovered box/<name>/ directory
	DefaultCandyDir = "candy"      // discovered candy/<name>/ directory
)

Project path constants shared by core and kit. Core aliases each via `const X = kit.X`.

View Source
const (
	KwRun        spec.StepKeyword = "run"
	KwCheck      spec.StepKeyword = "check"
	KwAgentRun   spec.StepKeyword = "agent-run"
	KwAgentCheck spec.StepKeyword = "agent-check"
	KwInclude    spec.StepKeyword = "include"
)
View Source
const CharlyNetworkName = "charly"

CharlyNetworkName is the shared bridge network used by all charly containers.

View Source
const ContainerInfraErrMarker = "container-setup infra failure"

ContainerInfraErrMarker is the stable substring the container executor stamps into a container-setup infra error, mirroring SignalKillErrMarker. probeWasContainerInfra (eventually.go), the check-box exit mapping, and the reporter all key on it — one literal, no parallel copies (R3).

View Source
const DefaultCheckLevel = CheckLevelNoAgent

DefaultCheckLevel is the rung applied when a box declares no check_level.

View Source
const DeployConfigEnv = "CHARLY_DEPLOY_CONFIG"

DeployConfigEnv overrides the per-host deploy-config PATH. A check bed sets it so a disposable run never touches the operator's real ~/.config/charly/charly.yml.

View Source
const GraderDefaultTimeout = 5 * time.Minute

GraderDefaultTimeout bounds a single grader invocation when neither the `charly check feature run --timeout` flag nor the AI entry's own `timeout:` is set. Unlike the plateau-bounded harness loop, a grader call MUST be wall-clock-bounded so one stuck prose step can't hang an acceptance run.

View Source
const HostVar = "HOST"

HostVar is the cross-member address variable name — ${HOST:<member>} (+ optional :port). An unresolved one means the member is UNREACHABLE (a real failure, never a skip).

View Source
const LedgerSchemaVersion = "2026.161.1649"

LedgerSchemaVersion is the install-ledger record format version, DECOUPLED from the project schema HEAD so a non-ledger cutover never invalidates a migrated ledger. Read by core's ledger path (ReadDeployRecord/ReadCandyRecord, which hard-reject a record lacking this stamp). Core aliases via `const … = kit.LedgerSchemaVersion`.

View Source
const PodmanInfraExitCode = 125

PodmanInfraExitCode is podman's own "an error occurred in podman itself" exit code — container create/store/config failures the CLI reports before (or instead of) running the container command. It is the PRIMARY structured infra signal. (podman/libpod registerExitCode: 125 = "the error is with podman itself".)

View Source
const ProcessShutdownGrace = 2 * time.Second

ProcessShutdownGrace bounds each stage of the graceful process-group shutdown (ShutdownProcessGroup). Named + documented, not a tuned magic value: it is the guarantee that closing a stdio-carried process ALWAYS terminates instead of wedging the caller on a child that ignores its stdin EOF.

View Source
const SignalKillErrMarker = "terminated by signal"

RunCaptureCmd is the shared output-capture helper. Identical behaviour to the pre-cutover testrun.go's runCapture (which lived on the now- deleted Executor interface): exit codes are NOT errors, only spawn failures are. Lives here so SSHExecutor / NestedExecutor implementations can share it without circular imports. SignalKillErrMarker is the stable substring every RunCaptureCmd caller stamps into a signal-kill error (see below). The check runner's probeWasKilled matches it to tell a probe that was KILLED before completing (infra interruption → re-attempt) from a probe that RAN and returned a failure (authoritative → never retried). One shared literal (R3).

View Source
const VenueLocal = "local"

VenueLocal is the stable Venue() identifier for the local host. Exported so install_ledger.go and tests can reference it without hard-coding the literal.

Variables

View Source
var ClassifyDoc = spec.ClassifyDoc

ClassifyDoc inspects a document's top level and returns its shape. See spec.ClassifyDoc.

View Source
var ContainerRunning = defaultContainerRunning

ContainerRunning reports whether a container is running. Package-level var for testability (tests inject a stub, same pattern as EnsureCharlyNetwork/InspectLabels).

View Source
var EnsureCharlyNetwork = defaultEnsureCharlyNetwork

EnsureCharlyNetwork creates the "charly" network if it does not exist. It is a package-level var for testability.

View Source
var ErrImageNotLocal = errors.New("image not found in local storage")

ErrImageNotLocal is returned when a user-supplied image reference cannot be resolved against local engine storage. Promoted here (from charly/labels.go) because ResolveLocalImageRef is the function that returns it, and charly core's ~12 other callers of the local-image resolution surface now reference kit.ErrImageNotLocal directly (no re-export alias — ZERO-ALIASES).

View Source
var ErrLockBusy = errors.New("file lock held by another process")

ErrLockBusy is returned by a NON-blocking AcquireFileLock when another holder already owns the lock. Callers detect it with errors.Is to render a precise "already in progress" message.

View Source
var InspectContainer = defaultInspectContainer

InspectContainer is swappable for tests. Real calls shell out to `<engine> inspect <name>` (which returns a one-element JSON array).

View Source
var InspectLabels = InspectImageLabels

InspectLabels reads OCI labels from a local image via engine inspect. Package-level var for testability — defaults to the canonical InspectImageLabels (kit/local_image.go, promoted from charly/labels.go by the parallel K3 leg) rather than a duplicate body: this file originally carried its own byte-identical defaultInspectLabels, a genuine R3 duplicate produced by two independent extractions of the same charly-core function racing each other (caught during PR validation).

View Source
var ListLocalImages = defaultListLocalImages

ListLocalImages returns all images in the engine's local storage. Package-level var for testability (same pattern as LocalImageExists, DetectGPU).

View Source
var LocalImageExists = defaultLocalImageExists

LocalImageExists checks whether an image reference exists in the given engine's local store. Package-level var for testability (same pattern as DetectGPU in gpu.go).

View Source
var MappingRoot = spec.MappingRoot

MappingRoot unwraps a YAML document node to its top-level mapping node (or nil). Relocated to sdk/spec (FLOOR-SLIM axis-A mechanical batch, zero logic change); aliased here so every existing kit.MappingRoot call site (charly core's former mappingRoot alias + the out-of-module candy/plugin-migrate engine) keeps compiling unchanged (R3).

View Source
var PluginPrimaries = map[string]string{

	"cdp": "method", "wl": "method", "dbus": "method", "vnc": "method",
	"mcp": "method", "record": "method", "spice": "method", "libvirt": "method",
	"kube": "method", "adb": "method", "appium": "method",
}

PluginPrimaries maps a plugin verb word to its declared PRIMARY input field — the target of the scalar sugar shorthand (`file: /usr/bin/xterm` → plugin_input: {file: …}). Compiled-in plugins seed it at init via RegisterPluginPrimary (their capability manifest); the byte-gated prescan registers an external plugin's declared primary before parse.

View Source
var (
	PollRemote = vmshared.PollRemote
)
View Source
var ReadinessProvider = func() ResolvedReadiness {
	rr, _ := vmshared.ResolveReadiness(nil)
	return rr
}

ReadinessProvider returns the resolved readiness bounds the executors' waits use. Defaults to the built-in bounds; charly overrides it at init with its project-aware loadedReadiness (charly/readiness_config.go).

View Source
var RuntimeConfigPath = defaultRuntimeConfigPath

RuntimeConfigPath returns the path to the user's runtime config file.

View Source
var RuntimeOnlyVarPrefixes = []string{
	"HOST_PORT",
	"VOLUME_PATH",
	"VOLUME_CONTAINER_PATH",
	"CONTAINER_IP",
	"CONTAINER_NAME",
	"INSTANCE",
	"ENV_",

	"STEP_ID",

	"VM_HOSTDEV_COUNT",

	"DEPLOY_NAME",

	"HOST",
}

RuntimeOnlyVarPrefixes lists variable name prefixes that are only resolvable against a running container. scope:"build" checks must not reference these.

View Source
var ScalarNode = spec.ScalarNode

ScalarNode builds a string scalar YAML node. Relocated to sdk/spec (FLOOR-SLIM axis-A mechanical batch, zero logic change) so charly core can call it without importing kit; aliased here so every existing kit.ScalarNode call site (candy/plugin-migrate + others) keeps compiling unchanged.

View Source
var SudoLocalImageExists = defaultSudoLocalImageExists

SudoLocalImageExists checks whether an image reference exists in the rootful (sudo podman) local store. Mirrors LocalImageExists but always queries the root user's storage namespace, regardless of the caller's BuildEngine. The rootless and rootful podman storage roots are isolated, so an image built by the user's `podman build` is invisible to `sudo podman` until transferred.

Package-level var for testability (same pattern as LocalImageExists).

View Source
var SystemdUserRuntimeDir = func() string {
	return filepath.Join("/run/user", strconv.Itoa(os.Geteuid()), "systemd")
}

SystemdUserRuntimeDir returns the path the directory check probes — `/run/user/<uid>/systemd`. Exposed as a package-level var so tests can redirect to a t.TempDir().

View Source
var TestVarRefPattern = regexp.MustCompile(`\$\{([A-Z_][A-Z0-9_]*)(?::([^}]+))?\}`)

TestVarRefPattern matches a ${NAME} or ${NAME:arg} reference (uppercase-underscore names).

View Source
var ValidateRecord = func(kind, label string, v any) error { return nil }

ValidateRecord is the egress-validation seam for ledger writes. The ledger (install_ledger.go) validates each record against its egress schema before writing; charly injects its ValidateEgressValue here at init (see charly/install_ledger_egress.go). Defaults to a no-op for standalone kit use.

Functions

func AcquireFileLock

func AcquireFileLock(path string, blocking bool) (release func() error, err error)

AcquireFileLock takes an advisory flock on path (creating the file + parent dirs on demand) and returns a release closure that unlocks + closes.

blocking selects the contention behavior:

  • true → LOCK_EX: wait until the lock is free (serialize, never fail).
  • false → LOCK_EX|LOCK_NB: return ErrLockBusy immediately when another holder exists.

The lock file is deliberately NOT unlinked on release (unlinking a held lock races a waiter that already opened the prior inode). flock is per-open-file-description, so two acquires of the same path — even within ONE process — contend, which the duplicate-run guard relies on.

func AcquireImageBuildLock added in v0.2026193.1308

func AcquireImageBuildLock(fullTag string) (func() error, error)

AcquireImageBuildLock takes the blocking per-image build lock for fullTag.

func AcquireLocalPkgBuildLock added in v0.2026190.848

func AcquireLocalPkgBuildLock(srcDir string) (func() error, error)

AcquireLocalPkgBuildLock serializes concurrent localpkg builds sharing a source dir.

func AcquireVmDomainLock added in v0.2026201.1934

func AcquireVmDomainLock(domain string) (func() error, error)

AcquireVmDomainLock takes a BLOCKING, host-global advisory lock serializing every check bed that occupies the given libvirt domain. Host-global (under ~/.cache/charly/.locks/) because the qemu:///session domain namespace is host-wide, shared across project dirs.

func AddBox added in v0.2026193.1241

func AddBox(dir, name, base string, layers []string) error

AddBox writes a new box to its discovered per-box file box/<name>/charly.yml as a node-form IMAGE — `<name>: {candy: {base: …}}`. The base argument is the value of the image's `base:` field (an external URL or the name of another box). If layers is non-nil it populates the image's `candy:` composition list. Errors if box/<name>/charly.yml exists.

func AddCandyDeployment added in v0.2026190.848

func AddCandyDeployment(paths *LedgerPaths, candyName, deployID string, update func(*CandyRecord)) error

AddCandyDeployment adds deployID to candy.DeployedBy and writes the record. Used at install time.

func AddCandyDeploymentVia added in v0.2026190.848

func AddCandyDeploymentVia(exec spec.DeployExecutor, paths *LedgerPaths, candyName, deployID string, update func(*CandyRecord)) error

AddCandyDeploymentVia is the executor-routed variant of AddCandyDeployment. When exec is nil or a local executor, it falls back to operator-side file I/O (today's behaviour). When exec is a non-local DeployExecutor (SSHExecutor / NestedExecutor), the ledger file I/O goes through exec.GetFile + exec.RunSystem so the ledger lands on the substrate's filesystem under the substrate's ~/.config/opencharly/installed/ — matching the install's actual venue (arch-vm.arch-host writes in the arch VM guest; sway-pod with nested pods writes in the parent pod; etc.).

func AppendUnique added in v0.2026198.345

func AppendUnique(dst []string, items ...string) []string

AppendUnique appends items to dst, skipping any already present in dst.

func ApplyPortOverrides added in v0.2026190.848

func ApplyPortOverrides(ports []string, overrides []string) ([]string, error)

ApplyPortOverrides modifies port mappings based on --port flags. Each override is "newHost:containerPort". It replaces the host port for the matching container port in the ports list. Preserves protocol suffixes like /udp.

func AtomicWriteFile added in v0.2026192.1709

func AtomicWriteFile(path string, data []byte, perm os.FileMode) error

AtomicWriteFile writes data to path atomically: a temp file in the SAME dir (same filesystem, so rename is atomic) is written, chmod'd, then renamed over path. A concurrent reader sees either the old complete file or the new complete file, never a partial write; concurrent writers of identical content converge (last rename wins, bytes identical). Relocated from charly core (P8) so the build render engine (sdk/deploykit) and charly's staging primitives share it.

func BareVolumeName added in v0.2026197.1320

func BareVolumeName(volumeName, boxName, instance string) string

BareVolumeName strips the "charly-<box>[-<instance>]-" prefix from a resolved volume name.

P12a: relocated from sdk/deploykit/quadlet.go (a pure string helper with no deploykit-specific coupling) so this file's mergeRuntimeVars can call it without a kit→deploykit import (deploykit already imports kit — the reverse direction would cycle). deploykit's own caller now calls kit.BareVolumeName.

func BedVmDomains added in v0.2026201.1934

func BedVmDomains(name string, node spec.BundleNode) []string

BedVmDomains returns the sorted, deduped libvirt domain names (charly-<from>) a bed's VM(s) occupy — the bed's own vm target plus any group-member vm targets. This is the unit of exclusive host contention two DISTINCT beds can collide on (the per-domain lock in AcquireVmDomainLock serializes them).

func BuildArchExports

func BuildArchExports() string

BuildArchExports emits the BUILD_ARCH=$(uname -m) + ARCH=<buildkit-triplet> shell preamble so a cmd:/download: body can template ${ARCH}/${BUILD_ARCH} at deploy-time the same way the container build gets them from BuildKit's TARGETARCH.

func BuildBuilderRunArgs added in v0.2026190.848

func BuildBuilderRunArgs(opts BuilderRunOpts) []string

BuildBuilderRunArgs assembles the podman/docker run argv. Extracted for unit-testability — we assert the produced argv rather than actually spawning a container in tests.

func BuildPortMapping added in v0.2026198.345

func BuildPortMapping(boxPorts []string) map[int]int

BuildPortMapping builds a host→container port map from image port mappings. Same loud-failure policy as ParseHostPorts — see comment above.

func BuilderCollectContext

func BuilderCollectContext(word string, in spec.BuilderCollectInput) map[string]any

BuilderCollectContext returns the builder-specific stage-context keys for `word` given the host-supplied candy descriptor. An unknown word returns nil (a custom candy builder with no plugin keeps base-only context — the host never invokes a plugin for it).

func BuilderResolve

func BuilderResolve(word string, in spec.BuilderResolveInput) (spec.BuilderResolveReply, error)

BuilderResolve renders `word`'s build-time multi-stage from the host-supplied context, returning the pieces the host splices into the Containerfile: Stage (pre-main-FROM), CopyArtifacts + CopyBinary (post-main-FROM), or InlineFragment (in-candy, inline builders). An unknown word is a LOUD error (never a silent empty stage). This is the ONE render both the box-build plugin OpResolve and the in-proc pod-overlay build-emit call (R3).

func BuilderReverse

func BuilderReverse(word string, in spec.BuilderReverseInput) []spec.ReverseOp

BuilderReverse returns the teardown ops for `word` given its resolved stage context (the BuilderCollectContext output the host stored on the BuilderStep). An unknown word, or a context missing the keys a builder needs, returns nil (no teardown — the same best-effort the in-proc builders had).

func BuilderRun added in v0.2026190.848

func BuilderRun(ctx context.Context, opts BuilderRunOpts) ([]byte, error)

BuilderRun runs the configured builder container. Returns the command's combined stdout+stderr on success; on failure, returns the output plus the exec error.

func CheckLevelReaches added in v0.2026191.1837

func CheckLevelReaches(have, want string) bool

CheckLevelReaches reports whether a box resolved to `have` runs at least as deep as `want` — e.g. CheckLevelReaches(boxLevel, CheckLevelNoAgent) gates the runtime live pass, CheckLevelReaches(boxLevel, CheckLevelAgent) gates the agent grader. Both operands are normalized through ResolveCheckLevel first.

func ClassifyContainerInfraFailure added in v0.2026194.2258

func ClassifyContainerInfraFailure(exitCode int, stderr string) (signature string, ok bool)

ClassifyContainerInfraFailure reports whether a NONZERO container-run/exec result is podman's OWN infra failure (the container never ran the check command), returning the matched signature (with provenance) for annotation. exit-125 is the primary signal; the stderr table is the secondary discriminator. A zero exit is never infra.

func ClassifyStepFailures added in v0.2026197.1537

func ClassifyStepFailures(results []StepResult) (checkFails, infraFails int)

ClassifyStepFailures splits FAIL results into genuine check failures vs. container-setup INFRA failures (IsContainerInfraResult) — the shared discriminator every check-run caller (box/live/feature, in-core and externalized) uses to map its own exit-code error type. An infra failure means the check command never ran (a probe container's mount/passwd-gen raced concurrent build churn); it must never read as a checks-failed verdict.

func CloseHostCleanups added in v0.2026197.1537

func CloseHostCleanups(cleanups []func())

CloseHostCleanups tears down any ssh -L forwards opened while resolving ${HOST:<member>} address variables. Safe to call on a nil/empty slice.

func CollectAnyStrings added in v0.2026191.2033

func CollectAnyStrings(v any) []string

CollectAnyStrings returns every string within a plugin_input value (scalar string / nested map / list), depth-first. The READ-ONLY analogue of ExpandAnyVars: it lets the ${HOST:…} cross-member scan (collectHostRefs) reach a plugin verb's authored fields, which live in the opaque PluginInput map rather than StringFields.

func CollectHostRefs added in v0.2026197.1537

func CollectHostRefs(checks []spec.Op) []string

CollectHostRefs returns the distinct ${HOST:<member>} variable keys referenced across every string field of every check (keys in the "NAME:arg" form used by ExpandTestVars).

func CompareCalVer added in v0.2026192.1709

func CompareCalVer(a, b string) int

CompareCalVer compares two CalVer strings numerically component-by-component, falling back to lexical comparison for any non-numeric component. Returns -1 if a < b, +1 if a > b, 0 if equal. Relocated from charly core (P8) so the build render engine (sdk/deploykit) and charly's image-tag logic share ONE comparator. Distinct from CalVer.Less, which requires strictly-canonical parsed CalVers; this is the lenient dotted-string comparator the build/tag paths use.

func CompareSemver added in v0.2026192.1026

func CompareSemver(a, b string) int

CompareSemver compares two semver-like version strings (e.g. "v1.2.3"). Returns -1 if a < b, 0 if equal, 1 if a > b. Handles v-prefixed versions and falls back to string comparison for non-numeric parts.

func ContainerImage added in v0.2026198.358

func ContainerImage(engine, containerName string) string

ContainerImage returns the image ref for a running container, best-effort ("" on error). Thin wrapper over ContainerImageRef.

func ContainerImageRef added in v0.2026198.358

func ContainerImageRef(engine, containerName string) (string, error)

ContainerImageRef returns the image ref backing a running container (.Config.Image via `<engine> inspect`). THE single container→image-ref inspector — there is exactly one inspect implementation.

func ContainerInfraError added in v0.2026194.2258

func ContainerInfraError(signature string, exitCode int, stderrPreview string) error

ContainerInfraError is the exported constructor for a classified CONTAINER-CREATE infra failure — the R44 Option-A box-mode creates ONE persistent container host-side, and its create failure is classified there (not through the per-step RunCapture seam). The caller returns this as the check's error so it exits the INFRA class (a plain error → exit 1), never checks-failed, and the marker makes it recognizable in logs.

func ContainerName added in v0.2026190.848

func ContainerName(boxName string) string

ContainerName returns the deterministic container name for an image or a `<base>/<instance>` deploy key (the `/` is canonicalized to `-`).

func ContainerNameInstance added in v0.2026190.848

func ContainerNameInstance(boxName, instance string) string

ContainerNameInstance returns the container name with an optional instance suffix.

func ContainerPortsFromMappings added in v0.2026190.848

func ContainerPortsFromMappings(mappings []string) []int

ContainerPortsFromMappings extracts the container-side port number from each mapping. "auto" sentinels are skipped (they have no container port to extract — they ARE the request to allocate one). Unparseable entries are silently dropped (the loud-skip warning lives in CheckPortAvailability).

func DecodeInput

func DecodeInput(in map[string]any, out any)

DecodeInput decodes an Op's plugin_input (map[string]any) into a candy's CUE-generated typed params struct via a JSON round-trip. A nil/empty input leaves out at its zero value; the host has already validated the input against the served schema.

func DefaultDeployConfigPath added in v0.2026186.124

func DefaultDeployConfigPath() (string, error)

DefaultDeployConfigPath returns the per-host deploy overlay file (~/.config/charly/charly.yml), honoring the DeployConfigEnv override.

func DeleteCandyRecord added in v0.2026190.848

func DeleteCandyRecord(paths *LedgerPaths, layer string) error

DeleteCandyRecord removes candy/<candy>.json.

func DeleteDeployRecord added in v0.2026190.848

func DeleteDeployRecord(paths *LedgerPaths, id string) error

DeleteDeployRecord removes deploys/<deploy-id>.json; silently ignores not-found (teardown is idempotent).

func DescentFromTraits added in v0.2026192.2018

func DescentFromTraits(t *spec.DeployTraits) *spec.DescentDescriptor

DescentFromTraits builds a #DescentDescriptor from a substrate's DECLARED #DeployTraits, deriving the closed nesting-transport vocabulary GENERICALLY from the traits — never by switching on a substrate kind word. It copies the declared traits verbatim (the consult sites read them off node.Descent) and computes transport + host_rooted:

leaf_only            → reject          (k8s: a deploy-chain leaf, unreachable via exec)
venue == container   → container-exec  (pod: podman/docker exec by name)
venue == ssh         → ssh             (vm: an ssh hop into the guest)
otherwise            → none            (shell/parent/none share the parent venue)

nil traits (a targetless group / empty target / a word with no declared substrate traits) map to the external-in-place default (container-exec) — behaviour-preserving for the former descentForTarget default arm that covered pod and the empty/group target.

func DescriptorFromExecutor added in v0.2026203.438

func DescriptorFromExecutor(exec spec.DeployExecutor) spec.VenueDescriptor

DescriptorFromExecutor is the pure INVERSE of VenueFromDescriptor: it derives a serializable spec.VenueDescriptor from an already-materialized, LIVE executor value. Only the two round- trippable shapes VenueFromDescriptor understands ("shell"/"ssh") are recognized; any other concrete type (e.g. a composed *NestedExecutor, which cannot be flattened into one {kind,host,port} tuple) returns the zero VenueDescriptor{} — callers treat that exactly like VenueFromDescriptor's own "" case (no venue; keep whatever executor is already in hand).

The bed-regression fix this promotion serves (FIX ROUND, S3b follow-up): a NESTED external deploy's ancestor executor (deploykit.RootExecutorForDeployNode's "ssh" result, threaded core- side as EmitOpts.ParentExec via the ancestor-chain walk in charly/bundle_add_cmd.go's deriveChildExecutorForPath) is ALWAYS a plain ShellExecutor/*SSHExecutor for a single hop into a vm guest — never a NestedExecutor — so it round-trips through this exact pair of functions. charly/unified_targets.go's pluginDeployTarget.Add uses this to convert that live ancestor executor into venue_json BEFORE dispatch, since a live Go interface value cannot itself cross the []byte wire to candy/plugin-bundle (mirrors candy/plugin-bundle's OWN identically-shaped former venueDescriptorFromExecutor, now deleted — R3, one function for both directions' callers).

func DetectEngine added in v0.2026190.848

func DetectEngine() (string, error)

DetectEngine auto-detects the container engine: prefers podman, falls back to docker.

func DetectRunMode added in v0.2026190.848

func DetectRunMode(runEngine string) string

DetectRunMode returns "quadlet" when podman is present AND a functional systemd-user session is reachable (systemctl binary + XDG_RUNTIME_DIR + /run/user/<uid>/systemd directory). Otherwise returns "direct".

The functional-systemd-user check (added 2026-04-27) catches nested environments — harness sandbox pods, supervisord-only containers, sysvinit hosts — that have the systemctl binary present but no running `systemd --user` session. Without this check, `charly bundle add <name> <ref>` would silently pick run_mode=quadlet, write the .container file, and fail at `systemctl --user daemon-reload` time. With the check, run_mode=direct is auto-selected on those hosts and `runConfigDirect()` (in config_image.go) emits a `podman run -d` invocation instead.

func DirExists

func DirExists(path string) bool

DirExists reports whether path exists and is a directory.

func DirectDeployMarkerDir added in v0.2026198.345

func DirectDeployMarkerDir() (string, error)

DirectDeployMarkerDir returns ~/.config/charly/direct/, the registry directory for direct-mode deploys (the equivalent of ~/.config/containers/systemd/ for quadlet deploys).

func DirectDeployMarkerPath added in v0.2026198.345

func DirectDeployMarkerPath(box, instance string) (string, error)

DirectDeployMarkerPath returns the marker JSON path for a deploy.

func DiscoverRemoteCandy added in v0.2026192.1026

func DiscoverRemoteCandy(repoDir string) ([]string, error)

DiscoverRemoteCandy returns the list of candy names in a remote repo directory

func DiscoverSkipDir added in v0.2026194.1400

func DiscoverSkipDir(name string) bool

DiscoverSkipDir reports whether a directory name is a VCS or build-artifact dir that never contains a discoverable charly.yml manifest — skipped by the discover walk both for speed and to avoid traversing concurrently-mutated build outputs (e.g. a candy's pkgbuild/{pkg,src} under a live makepkg).

func DotenvLoaded added in v0.2026190.848

func DotenvLoaded(name string) bool

DotenvLoaded reports whether a given env var name was loaded from the project .env file.

func DownloadRepo added in v0.2026192.1026

func DownloadRepo(repoPath string, version string) (string, error)

DownloadRepo downloads a remote repo to the cache. Returns the cache path where the repo was stored.

Freshness contract: the cache is reused ONLY when its recorded provenance (the commit the export was cloned from) equals the ref's CURRENTLY resolved commit — so a branch that advanced upstream (main) is re-downloaded instead of silently serving stale content. The check costs the one ls-remote GitResolveRef already performs; for immutable tags the provenance always matches, so their cache-hit behavior is unchanged. A cache written before this contract has no provenance and is re-downloaded once, then self-heals.

func EffectiveStepID added in v0.2026191.2205

func EffectiveStepID(s *spec.Step, origin string, stepIdx int) string

EffectiveStepID returns the step's author id when set, else a derived id.

func EffectiveTags added in v0.2026191.1413

func EffectiveTags(stepTags []string) []string

EffectiveTags normalizes and de-dups a step's tags, preserving first-seen order. (Per-step tags only — there is no group-level tag inheritance.)

func EngineBinary added in v0.2026190.848

func EngineBinary(engine string) string

func EnrichNoProxy added in v0.2026190.848

func EnrichNoProxy(envs []string, containerNames []string) []string

EnrichNoProxy appends container hostnames to NO_PROXY when a proxy is configured. Chrome does not support CIDR ranges in NO_PROXY — only exact hostnames and domain suffixes. This ensures container-to-container traffic bypasses the proxy.

func EnsureCharlyInDeployVenue added in v0.2026200.1312

func EnsureCharlyInDeployVenue(ctx context.Context, ve spec.DeployExecutor, charlyBin, hostVer string) (string, error)

EnsureCharlyInDeployVenue is the host-side sibling of EnsureCharlyInVenue for a live spec.DeployExecutor. It gives every process-capable deployment the same Ansible-like bootstrap: keep an equal/newer packaged Charly, otherwise place the active controller binary at a versioned /tmp path and invoke that explicit path. It never changes PATH and never downgrades a target package. The underlying probe/compare/deliver decision remains ensureCharly (one copy).

func EnsureCharlyInGuest added in v0.2026186.739

func EnsureCharlyInGuest(ctx context.Context, ssh SSHArgs, charlyBin, hostVer, strategyRaw string) (string, error)

EnsureCharlyInGuest is the vm-deploy PrepareVenue-time coordinator: it layers the cloud-init charly_install.strategy over a HOST-SURFACE delivery (os/exec ssh/scp against the managed alias), because at PrepareVenue time the reverse channel has NOT yet served a guest executor. auto/scp: deliver the host charly when the guest's is absent/older; skip: verify presence only.

func EnsureCharlyInVenue added in v0.2026186.739

func EnsureCharlyInVenue(ctx context.Context, ve DeployExecutor, charlyBin, hostVer string) (string, error)

EnsureCharlyInVenue is the GENERIC copy-in over the DeployExecutor abstraction (container podman-cp / local cp / the reverse-channel guest executor) — used by the deploy walk + nested-pod delegation. The venue's PATH charly is used when at least as new as the host (NEVER shadowed/downgraded); otherwise charlyBin (the HOST charly, guaranteed current + from-box-capable, fed from HostEnv.CharlyBin) is delivered to /tmp/charly-<hostVer> (outside $PATH, invoked by explicit path).

func EnsureSshConfigInclude added in v0.2026186.739

func EnsureSshConfigInclude(home string) error

EnsureSshConfigInclude inserts the managed `Include ~/.config/charly/ssh_config` directive at the TOP of the user's ~/.ssh/config (creating it if needed) — the Include MUST be outside any Host block (ssh_config(5) lexical scoping), so it is PREPENDED. Idempotent.

func EnvMapToPairs added in v0.2026190.848

func EnvMapToPairs(env map[string]string) []string

EnvMapToPairs converts the deploy schema's env map into sorted KEY=VALUE pairs (the OCI-label wire + env-resolution chain form).

func EnvPairsToMap added in v0.2026190.848

func EnvPairsToMap(pairs []string) map[string]string

EnvPairsToMap converts KEY=VALUE pairs (the CLI -e / label wire form) into the map form the deploy schema stores since the env-shape unification.

func EnvdDir

func EnvdDir(home string) string

EnvdDir returns the directory where per-candy env files live under a home.

func EnvdFilePath

func EnvdFilePath(home, candyName string) string

EnvdFilePath returns the env file path for a given candy under a home.

func ExpandAnyVars added in v0.2026191.2033

func ExpandAnyVars(v any, env map[string]string) (any, []string)

ExpandAnyVars expands ${VAR} references in every string within a plugin_input value (scalar string / nested map / list), mutating maps and slices in place, and returns the (possibly rewritten) value plus the unresolved var names. Non-string scalars pass through untouched.

func ExpandHostHome added in v0.2026190.848

func ExpandHostHome(path string) string

ExpandHostHome expands ~ and $HOME in a path using the actual user's home directory.

func ExpandOpVars added in v0.2026191.2033

func ExpandOpVars(c *spec.Op, env map[string]string) []string

ExpandOpVars rewrites every ${...} reference on an Op in place using the supplied environment map. Returns the combined (sorted) list of unresolved refs encountered across all string fields.

func ExpandPath added in v0.2026190.848

func ExpandPath(path string, home string) string

ExpandPath expands ~, ${HOME} and $HOME in a path string to the given home directory. ${HOME} is replaced before bare $HOME so the braced form is handled (a bare $HOME ReplaceAll would not match "${HOME}").

func ExpandTestVars added in v0.2026191.2033

func ExpandTestVars(s string, env map[string]string) (string, []string)

ExpandTestVars substitutes ${NAME} and ${NAME:arg} references using the supplied environment map.

Keys in env for plain refs use just the name: env["HOME"] = "/home/user". Keys for parameterized refs combine name and argument with a colon: env["HOST_PORT:6379"] = "16379", env["VOLUME_PATH:workspace"] = "/var/lib/…".

Returns the substituted string and a list of unresolved refs (in encounter order, deduplicated). The caller decides whether unresolved refs are an error (build-time validation) or a skip reason (runtime).

func ExtractCalVerTag added in v0.2026197.1320

func ExtractCalVerTag(ref string) string

ExtractCalVerTag returns the CalVer portion of a ref's tag, or "" if the tag is not a recognisable CalVer (`YYYY.DDD.HHMM`). Lets the resolver distinguish CalVer tags from legacy floats like `:latest` (which should never be chosen as the newest candidate).

func ExtractMetadata added in v0.2026198.345

func ExtractMetadata(engine, imageRef string) (*spec.BoxMetadata, error)

ExtractMetadata reads OCI labels from a local image and returns parsed spec.BoxMetadata. Returns nil if the image has no ai.opencharly labels. Returns ErrImageNotLocal wrapped with the image ref if the image is not in local storage.

func FileExists

func FileExists(path string) bool

FileExists reports whether path exists and is a regular (non-dir) file.

func FillReverseUninstallCmds added in v0.2026190.848

func FillReverseUninstallCmds(ops []ReverseOp, renderUninstall func(format string, packages []string) string)

FillReverseUninstallCmds renders the host-venue uninstall command for every ReverseOpPackageRemove op in the slice from the format's uninstall_template (the embedded build vocabulary, charly/charly.yml), in place. Called at install/record time by the local deploy target and the external vm deploy (R3 — one shared filler) when the DistroConfig is in hand, so the persisted ledger op carries the exact removal command the teardown will run. Ops whose format declares no uninstall_template, or whose format isn't in the config, are left with an empty UninstallCmd (teardown then errors loudly rather than silently running a wrong command). The renderUninstall seam maps (format, packages) → the format's uninstall command, or "" when the format has no uninstall template. It keeps this function free of the buildkit DistroConfig/RenderTemplate dependency (which would cycle kit→buildkit→kit once reverse_ops lives in kit) — the caller (candy/plugin-bundle/deploy_target.go, S3b — was charly core's deploy_target_external.go before the deploy-dispatch cluster moved) closes over DistroCfg + RenderTemplate.

func FilterHostVars added in v0.2026191.2205

func FilterHostVars(missing []string) []string

FilterHostVars returns the ${HOST:…} cross-member references among the unresolved keys. An unresolved ${HOST:…} means the member is unreachable — a real failure, never a SKIP (a skip on an unreachable dependency is a fake pass). Other unresolved vars (a deploy-only var under build scope, an unmounted volume) stay a legitimate skip.

func FindEntityDirs added in v0.2026194.1400

func FindEntityDirs(path, filename string, recursive bool) ([]string, error)

FindEntityDirs walks a scan root and returns every directory that contains the given canonical filename. When recursive is false, only the immediate children of path are considered.

func FindMappingValue

func FindMappingValue(m *yaml.Node, key string) *yaml.Node

FindMappingValue returns the value node for key in a YAML mapping node, or nil. (Like MapValue, but requires the key node to be a scalar — the form the migration transforms + the core loader's legacy-shape detection both use.)

func FindPortOwner added in v0.2026190.848

func FindPortOwner(port int, engine string) (owner string, ownerType string)

FindPortOwner checks running containers to identify what is using a port.

func FirstUnmetDepStep added in v0.2026201.1934

func FirstUnmetDepStep(s spec.Step, verdictByID map[string]string) string

FirstUnmetDepStep returns the first dep id in s.DependsOn whose verdict is anything other than "pass" (or that is unknown / not yet run). Returns "" if every dep passed (or the step has no deps).

func FirstYAMLVersionLine

func FirstYAMLVersionLine(data []byte) string

FirstYAMLVersionLine extracts the value of the first top-level `version:` line.

func FormatHTTPHeaders added in v0.2026197.1320

func FormatHTTPHeaders(h http.Header) string

FormatHTTPHeaders renders an http.Header into a "Key: value\n" blob (one line per value, multi-value preserved) — the matcher-ready response-header form.

func FormatPortConflicts added in v0.2026190.848

func FormatPortConflicts(conflicts []PortConflict, image string) string

FormatPortConflicts produces a user-friendly error message with remediation suggestions.

func FormatPortMapping added in v0.2026190.848

func FormatPortMapping(p ParsedPortMapping) string

FormatPortMapping is the inverse of ParsePortMapping. Empty bindAddr / proto are omitted; trailing-zero / equal ports collapse to canonical short forms that podman accepts.

func FormatStepResultsJSON added in v0.2026191.1732

func FormatStepResultsJSON(w io.Writer, results []StepResult) error

FormatStepResultsJSON emits a structured JSON document.

func FormatStepResultsJUnit added in v0.2026191.1732

func FormatStepResultsJUnit(w io.Writer, results []StepResult) error

FormatStepResultsJUnit emits JUnit XML for CI dashboards. Steps surface as <testcase> grouped by origin into <testsuite>s.

func FormatStepResultsTAP added in v0.2026191.1732

func FormatStepResultsTAP(w io.Writer, results []StepResult)

FormatStepResultsTAP emits TAP v13. Each step is one TAP test point.

func FormatStepResultsText added in v0.2026191.1732

func FormatStepResultsText(w io.Writer, results []StepResult)

FormatStepResultsText emits a human-readable per-step report to w.

func GPURunArgs added in v0.2026190.848

func GPURunArgs(engine string) []string

func GitClone added in v0.2026192.1026

func GitClone(repoURL string, ref string, commit string, targetDir string) error

GitClone clones a git repository at a specific ref into the target directory. Uses shallow clone for efficiency.

func GitDefaultBranch added in v0.2026192.1026

func GitDefaultBranch(repoURL string) (string, error)

GitDefaultBranch detects the default branch of a remote repository. Uses git ls-remote --symref to find what HEAD points to. Returns the branch name (e.g., "main", "master").

func GitLatestTag added in v0.2026192.1026

func GitLatestTag(repoURL string) (string, error)

GitLatestTag queries a remote repo for tags and returns the highest semver tag. Looks for tags matching v* pattern, sorts by semver, returns the highest. Returns an error if no version tags are found.

func GitResolveRef added in v0.2026192.1026

func GitResolveRef(repoURL string, ref string) (string, error)

GitResolveRef resolves a git reference (tag, branch, or commit) to a full commit hash. Uses git ls-remote for tags/branches; for commit hashes, validates length and returns as-is.

func HTTPClientFor added in v0.2026197.1320

func HTTPClientFor(base *http.Client, req HTTPRequest) (*http.Client, error)

HTTPClientFor builds a per-request *http.Client honoring the HTTPRequest policy (AllowInsecure, NoFollowRedirects, CAPEM, Timeout), derived from the engine's base client. The base supplies the default timeout; req.Timeout overrides it.

func HostCharlyIsNewer added in v0.2026186.739

func HostCharlyIsNewer(hostVer, venueVerOut string) bool

HostCharlyIsNewer reports whether the host charly (hostVer) is STRICTLY newer than a venue's charly (venueVerOut = raw `charly version` stdout). The single CalVer arbiter (R3): a venue version that is unparseable/absent → host wins (treated as older); both parse → strict CalVer compare (host newer → true, venue equal-or-newer → false, never downgraded); host unparseable → false (never clobber a venue charly on an unprovable claim).

func HumanBytes added in v0.2026202.1944

func HumanBytes(n int64) string

HumanBytes renders a byte count as a compact human-readable string.

func ImageBuildLockPath added in v0.2026193.1308

func ImageBuildLockPath(fullTag string) (string, error)

ImageBuildLockPath is the pure per-image build-lock key derivation: the user-cache lock file for an image ref, with the :tag stripped (preserving any registry:port colon) so every CalVer build of one image shares ONE lock — a shared intermediate built COLD once while distinct leaves fan out in parallel. Shared across the module boundary (R3) so charly core's acquireImageBuildLock AND the compiled-in candy/plugin-build DRIVE derive the byte-identical path.

func InputStr added in v0.2026187.747

func InputStr(c *spec.Op, key string) string

InputStr reads a string field from the step's desugared plugin input map — the per-verb fields live there since the schema-compaction cutover (the `<word>: <input>` sugar desugars to Plugin/PluginInput; core #Op carries no per-verb fields).

func InspectImageLabels added in v0.2026198.254

func InspectImageLabels(engine, imageRef string) (map[string]string, error)

InspectImageLabels reads a local image's OCI labels via engine inspect (promoted here from charly/labels.go's defaultInspectLabels, K3 reentry-class dissolution — box_labels_cmd.go's ONLY host-exclusive need was this + ResolveRuntime/ResolveLocalImageRef/LocalImageExists, all already kit-owned, so candy/plugin-box's `labels` command now calls this directly and the `__box-labels` reentry is gone). Pure container-storage probe: no charly-core coupling.

func InstallDirAtomic added in v0.2026193.1801

func InstallDirAtomic(tmp, final string) error

InstallDirAtomic atomically installs the freshly-populated tmp directory as final. When final already exists, the two dirs are swapped in a single atomic renameat2(RENAME_EXCHANGE) — a concurrent reader of final always sees a complete dir (the old one before, the new one after) — and the swapped-out old content (now under tmp) is removed. When final is absent, a plain rename installs it. A lost create-race (a concurrent process installed identical content first) is benign: the redundant tmp is discarded. Linux-only (renameat2); the project targets Linux. Relocated from charly core (P8b) so the build render engine and charly's staging primitives share the one primitive.

func IsAutoPort added in v0.2026190.848

func IsAutoPort(mapping string) bool

IsAutoPort reports whether a port-list entry is the literal "auto" sentinel. Authors write `port: [auto]` (or `port: [auto, "8443:443"]` to mix auto-allocation with explicit pins) in charly.yml.

func IsContainerInfraResult added in v0.2026194.2258

func IsContainerInfraResult(message string) bool

IsContainerInfraResult reports whether a CheckResult/StepResult message carries the infra marker (the executor stamped a container-setup infra error into it, propagated verbatim by the verbs' `execution error: %v` formatting). Keyed on the ONE marker (R3).

func IsDirectDeploy added in v0.2026198.345

func IsDirectDeploy(box, instance string) bool

IsDirectDeploy reports whether the named deploy was created in direct mode (i.e. has a marker file). Used by lifecycle commands.

func IsGitSubmoduleDir

func IsGitSubmoduleDir(p, root string) bool

IsGitSubmoduleDir reports whether p (≠ root) contains a .git entry (a nested submodule/repo boundary).

func IsHostNetworked added in v0.2026198.345

func IsHostNetworked(engine, containerName string) bool

IsHostNetworked checks if a running container uses --network host.

func IsMutableRef added in v0.2026201.852

func IsMutableRef(version string) bool

IsMutableRef reports whether a repo ref version can ADVANCE upstream (a branch name such as main, or an empty version that resolves to the default branch). Immutable coordinates — CalVer/semver tags (v…) and full commit SHAs — never change what they point at; the project's tags are add-only. Mutable refs must re-resolve on every access: a cache hit on a branch otherwise freezes it at first-download content forever (the pre-#146 @main protocol skew — a stale main export served protocol-v1 plugin sources indefinitely).

func IsRemoteImageRef added in v0.2026198.345

func IsRemoteImageRef(ref string) bool

IsRemoteImageRef returns true if a ref looks like a remote image reference (starts with @)

func IsRepoCached added in v0.2026192.1026

func IsRepoCached(repoPath, version string) (bool, error)

IsRepoCached checks if a repo version is already in the cache.

func IsRuntimeOnlyVar added in v0.2026191.2033

func IsRuntimeOnlyVar(key string) bool

IsRuntimeOnlyVar reports whether the given variable key (as returned by TestVarRefs) refers to a runtime-only value. The check matches on name prefix because parameterized vars share a common prefix with their arg.

func IsValidCheckLevel added in v0.2026191.1837

func IsValidCheckLevel(level string) bool

IsValidCheckLevel reports whether level is one of the four canonical rungs.

func KeywordOf added in v0.2026191.2205

func KeywordOf(s *spec.Step) spec.StepKeyword

KeywordOf returns the populated step keyword, or "" when none is set.

func ListVmSshAliases added in v0.2026186.739

func ListVmSshAliases(home string) ([]string, error)

ListVmSshAliases returns the alias names in the managed fragment, sorted.

func LoadProcessDotenv added in v0.2026190.848

func LoadProcessDotenv(dir string) error

LoadProcessDotenv loads .env from dir into the process environment. Variables already set in the environment are NOT overwritten (real env wins). Silently returns nil if .env does not exist.

func LoadWorkspaceEnv added in v0.2026190.848

func LoadWorkspaceEnv(workspace string) ([]string, error)

LoadWorkspaceEnv loads env vars from a workspace .env file (if it exists). Does NOT run direnv — direnv modifies the host env before charly runs. Returns nil, nil if no .env file found.

func LooksLikeFullRef added in v0.2026197.1320

func LooksLikeFullRef(ref string) bool

LooksLikeFullRef returns true if the image ref contains a registry segment (a "/" before any ":") — e.g. "ghcr.io/org/name:tag" — so it can be pulled without charly.yml resolution.

func ManagedBlockBody

func ManagedBlockBody(shell ShellKind, home string) string

ManagedBlockBody returns the shell-specific loop that sources the env.d directory under a home. POSIX-sh for bash/zsh, fish syntax for fish.

func ManagedBody added in v0.2026186.739

func ManagedBody(text string) string

ManagedBody returns just the contents between the begin/end fence markers (the global, untagged managed block) in text. Returns "" when the markers are absent.

func MapValue

func MapValue(m *yaml.Node, key string) *yaml.Node

MapValue returns the value node for key in a YAML mapping node, or nil.

func MappingChild added in v0.2026186.1019

func MappingChild(m *yaml.Node, key string) *yaml.Node

MappingChild looks up a key in a mapping node. Returns the value node or nil if missing. yaml mapping nodes store [key, value, key, value, …].

func MarkersForTag

func MarkersForTag(marker string) (begin, end string)

MarkersForTag returns the begin/end fence pair for a marker tag. Empty tag → the global-block fence; non-empty → a per-candy fence so multiple candies coexist.

func MigrateCandidateYAMLFiles

func MigrateCandidateYAMLFiles(dir string, treeSubdirs []string) []string

MigrateCandidateYAMLFiles is the ONE candidate-file scanner the multi-document doc-migration steps share AND the core loader's legacy-vocab rejection scan uses: every `.yml`/`.yaml` under each of treeSubdirs (walked recursively, skipping nested git submodules + any `testdata` dir) plus the root-level YAML siblings in dir. Sorted, deduplicated.

func NestedContainerName added in v0.2026190.848

func NestedContainerName(path string) string

NestedContainerName maps a dotted deploy path (e.g. "stack.web") to the container/venue name form ("stack_web") used for nested-executor addressing.

func NodeShapedValue

func NodeShapedValue(val *yaml.Node) bool

NodeShapedValue reports whether a mapping node carries a reserved kind word as a key (i.e. it is a name-first node-form value).

func NormalizeTag added in v0.2026191.1413

func NormalizeTag(t string) string

NormalizeTag strips a single leading '@' so `@smoke` and `smoke` are identical — authors commonly write `@smoke` from Gherkin habit, but the sigil is optional in the YAML surface.

func OpUnifyCandidateFiles

func OpUnifyCandidateFiles(dir string) []string

OpUnifyCandidateFiles is the candidate-file set the op/plan-unify migrators AND the core loader's legacy-test-vocab rejection scan walk (candy/ + box/ trees + root siblings).

func ParseContainerPort added in v0.2026190.848

func ParseContainerPort(mapping string) (int, error)

ParseContainerPort extracts the container port from a mapping. Accepts every form ParsePortMapping does, including the IP:H:C bind-address form.

func ParseEnvBytes added in v0.2026190.848

func ParseEnvBytes(data []byte) ([]string, error)

ParseEnvBytes parses KEY=VALUE entries from raw bytes. Skips comments (#), blank lines, and strips surrounding quotes from values.

func ParseEnvFile added in v0.2026190.848

func ParseEnvFile(path string) ([]string, error)

ParseEnvFile reads a .env file and returns KEY=VALUE strings. Skips comments (#), blank lines, and supports KEY=VALUE and KEY="VALUE" (strips quotes). Compatible with docker --env-file format.

func ParseHostPort added in v0.2026190.848

func ParseHostPort(mapping string) (int, error)

ParseHostPort extracts the host port from a mapping. Accepts every form ParsePortMapping does, including the IP:H:C bind-address form.

func ParseHostPorts added in v0.2026198.345

func ParseHostPorts(boxPorts []string) []int

ParseHostPorts extracts host-side ports from image port mappings via the canonical ParsePortMapping. Unparseable entries are reported on stderr — silent skipping was the root cause of an unrelated bug where tunnel rules vanished without a diagnostic.

func ParsePublishedPort added in v0.2026197.1537

func ParsePublishedPort(output string, port int) (string, error)

ParsePublishedPort extracts a `podman port <container> <N>` command's output (one "host:port" line, or "0.0.0.0:port" / "[::]:port" forms) into a dialable 127.0.0.1-normalized host:port string. P12a: relocated from charly/check_venue.go (a pure string-parsing helper with no host state; its sole caller stays core).

func ParseTaskMode

func ParseTaskMode(mode string, def uint32) uint32

ParseTaskMode parses a candy task mode string ("0644","0o755") into a uint32 file mode, falling back to def when empty/unparseable.

func PluginPrimaryFor added in v0.2026198.345

func PluginPrimaryFor(word string) (string, bool)

PluginPrimaryFor returns word's declared primary input field.

func Plural added in v0.2026191.1732

func Plural(n int) string

Plural returns "" for n==1 and "s" otherwise — the trivial English pluralization used by the result reporters and the eventually-retry summaries.

func PodName added in v0.2026198.345

func PodName(boxName string) string

PodName returns the container name for a pod's primary container.

func PodNameInstance added in v0.2026198.358

func PodNameInstance(boxName, instance string) string

PodNameInstance returns the container name for a pod's primary container, instance-aware.

func PodQuadletFilename added in v0.2026201.1135

func PodQuadletFilename(boxName string) string

PodQuadletFilename returns the quadlet filename for a pod (K4: relocated from charly/quadlet_pod.go — pure string formatting, no project-loader dependency).

func PodQuadletFilenameInstance added in v0.2026201.1135

func PodQuadletFilenameInstance(boxName, instance string) string

PodQuadletFilenameInstance returns the quadlet filename for a pod with optional instance.

func PosArtifact

func PosArtifact(c *spec.Op) []string

func PosCommandFields

func PosCommandFields(c *spec.Op) []string

PosCommandFields splits the input `command` into argv slots, prefixed with `--` so kong does not treat embedded -flags as its own (libvirt:guest/exec). For shell metachars use `command: "sh -c '<full>'"`.

func PosKeyNameSplit

func PosKeyNameSplit(c *spec.Op) []string

PosKeyNameSplit splits the input `key` on whitespace (libvirt send-key: "ctrl alt F2" → 3 slots).

func PosLibvirtQmp

func PosLibvirtQmp(c *spec.Op) []string

PosLibvirtQmp emits a QMP method name + optional JSON args (input text = method, input = JSON).

func PosTarget

func PosTarget(c *spec.Op) []string

func PosText

func PosText(c *spec.Op) []string

func QuadletDir added in v0.2026201.1135

func QuadletDir() (string, error)

QuadletDir returns the user-level quadlet directory.

func QuadletExists added in v0.2026201.1135

func QuadletExists(boxName string) (bool, error)

QuadletExists checks whether a .container file exists for the given image.

func QuadletExistsInstance added in v0.2026201.1135

func QuadletExistsInstance(boxName, instance string) (bool, error)

QuadletExistsInstance checks whether a .container file exists for the given image/instance.

func QuadletFilename added in v0.2026201.1135

func QuadletFilename(boxName string) string

QuadletFilename returns the quadlet filename for an image.

func QuadletFilenameInstance added in v0.2026201.1135

func QuadletFilenameInstance(boxName, instance string) string

QuadletFilenameInstance returns the quadlet filename for an image with optional instance.

func ReexecOverSSH added in v0.2026203.438

func ReexecOverSSH(host, identityFile string, options []string, controllerBin, version string, wantTTY bool) int

ReexecOverSSH rewrites os.Args by stripping --host and the client- local path flags (--dir/-C, --repo), resolves the remote charly endpoint (the venue's own PATH charly when it is at least as new as the local controller; otherwise a version-gated replica of the local binary delivered by EnsureCharlyInDeployVenue), then invokes `ssh <resolved-target> <endpoint> <rest of argv>`. Stdin/stdout/stderr are piped straight through. The returned exit code is whatever `ssh` exits with (which propagates the remote `charly` exit code). The happy path prints nothing — a diagnostic appears only when the local binary is actually replicated or the bootstrap fails.

host/identityFile/options are the caller's --host/--host-identity-file/ --host-option flag values; controllerBin is the caller's OWN resolved active binary path (charly-core's activeCharlyBinary()); version is the caller's OWN CalVer identity (charly-core's CharlyVersion()); wantTTY is whether stdin is a terminal (charly-core's term.IsTerminal(stdin)) — all charly-core-only concerns the caller resolves and threads in, so this function stays pure stdlib+sdk/kit.

func RegisterPluginPrimary added in v0.2026198.345

func RegisterPluginPrimary(word, field string) error

RegisterPluginPrimary declares word's primary input field. A verb word that collides with an authored #Op field is rejected at registration — the sugar rule could never reach it (the field would classify as a builtin modifier).

func RemoteLaunchCommand added in v0.2026200.1312

func RemoteLaunchCommand(launch spec.ProcessLaunch) string

RemoteLaunchCommand renders one exact-argv launch as the single command string OpenSSH must receive: POSIX single-quoting each token preserves the argv boundary and prevents interpolation by the remote login shell. The working directory and environment belong to the TARGET side, so they ride inside the remote command (`cd <dir> && env <K=V>… <argv>`); applying them to the local ssh carrier would conflate controller and target state.

func RemoveImagesByReference added in v0.2026186.831

func RemoveImagesByReference(engineBin, reference string)

RemoveImagesByReference best-effort removes every local image whose repository BASENAME exactly equals `reference` (e.g. "<deploy>-overlay") via `<engineBin> images … | rmi`. Silent on error (image cleanup is best-effort). engineBin is the resolved engine binary (the host resolves "podman"/"docker"/"auto" and passes the concrete binary — kit does no detection).

The `--filter reference=<reference>` glob is NOT trusted alone: podman lists EVERY repo:tag of any matched image ID, so a base image or a cross-deploy image that shares content (same image ID as the overlay) leaks into the output. rmi'ing those blindly would destroy the base + unrelated deploys' images. So the emitted repo is re-checked in Go and only the EXACT `<reference>` repo is removed.

func RemoveManagedBlockAt added in v0.2026190.848

func RemoveManagedBlockAt(path, marker string) error

RemoveManagedBlockAt strips the managed block (tagged with marker) from the file at path, in place. A file left empty is removed; a missing file is a no-op. The file-level counterpart to StripManagedBlock (the pure string op).

func RemoveSshConfigInclude added in v0.2026186.739

func RemoveSshConfigInclude(home string) error

RemoveSshConfigInclude removes the managed Include line from ~/.ssh/config. Idempotent.

func RemoveVmSshStanza added in v0.2026186.739

func RemoveVmSshStanza(home string, alias string) (remaining int, err error)

RemoveVmSshStanza drops the named alias and returns the remaining count (0 → the caller also removes the Include). Idempotent.

func RenderDownloadScript

func RenderDownloadScript(op *spec.Op, candyVars map[string]string) string

RenderDownloadScript emits a shell snippet that fetches op.Download to a temp file, optionally extracts it into op.To, then cleans up — honoring the same flags the container build path respects (extract format, strip_components, include, mode, env). candyVars are exported alongside op.Env so a vars: key referenced in the URL resolves at deploy time.

func RenderEnvdBody

func RenderEnvdBody(candyName string, envVars map[string]string, pathAdd []string) string

RenderEnvdBody produces the deterministic, shell-agnostic POSIX-sh fragment for a candy's env vars + PATH additions. Sorted keys guarantee stable output.

func RenderManagedBlockStrip added in v0.2026190.848

func RenderManagedBlockStrip(path, marker string) string

RenderManagedBlockStrip is the remote analogue of RemoveManagedBlockAt: a POSIX-sh snippet that drops the marker's begin/end fence pair (and its body) from the file IN PLACE on the venue. The fences come from MarkersForTag so they match exactly what the forward walk wrote, and awk index() matches them LITERALLY (no regex). The final `cat > "$f"` rewrites in place to preserve the rc file's perms/owner.

func RenderOpCommand

func RenderOpCommand(op *spec.Op, ctxPath string, candyVars map[string]string) (string, bool)

RenderOpCommand turns an op into a shell command suitable for sudo/user execution. It handles every plugin-renderable verb EXCEPT copy (staged via the executor's PutFile) and the act-`plugin:` verb (whose ProvisionActor shell needs the in-proc registry — package main / RunHostStep render those). Returns (cmd, handled): handled=false means the op is a copy or an act-`plugin:` verb the caller must route elsewhere; handled=true with an empty cmd never occurs (every handled verb yields a body).

func ReplaceOrAppendManagedBlock

func ReplaceOrAppendManagedBlock(existing, body, marker string) string

ReplaceOrAppendManagedBlock replaces the begin/end fence pair's body (tagged with marker, empty for the global block) in existing, appending a fresh block at EOF when absent.

func ReplaceOrPrependManagedBlock added in v0.2026186.739

func ReplaceOrPrependManagedBlock(existing, body, marker string) string

ReplaceOrPrependManagedBlock replaces the marker-tagged managed block in place, or PREPENDS it (with a blank-line separator before the rest of the file) when absent. Used for the ssh-config Include line, which MUST be at the TOP of ~/.ssh/config (outside any Host block) — an appended Include would be gated on the last preceding Host stanza's match (ssh_config(5) lexical scoping).

func RepoCacheDir added in v0.2026192.1026

func RepoCacheDir() (string, error)

RepoCacheDir returns the cache directory for remote repos. Uses $CHARLY_REPO_CACHE env var if set, otherwise ~/.cache/charly/repos/.

func RepoCachePath added in v0.2026192.1026

func RepoCachePath(repoPath, version string) (string, error)

RepoCachePath returns the cache path for a specific repo version. e.g. ~/.cache/charly/repos/github.com/org/repo@v1.0.0/

func RepoGitURL added in v0.2026192.1026

func RepoGitURL(repoPath string) string

RepoGitURL converts a repo path to a git clone URL. e.g. "github.com/opencharly/ml-layers" -> "https://github.com/opencharly/ml-layers.git"

func ReportStepResults added in v0.2026197.1537

func ReportStepResults(w io.Writer, results []StepResult, format string)

ReportStepResults writes results in the requested format ("json"/"tap"/"junit", default text), dispatching to the FormatStepResults* family above. P12a follow-up: the SAME format-selection switch previously lived duplicated in charly/check_feature_run.go's reportSteps AND candy/plugin-check/check_cmd.go's reportSteps (R3) — both now delegate here, and candy/plugin-box's `box feature run` word (its move destination) calls it directly.

func ReportStepResultsCount added in v0.2026201.1934

func ReportStepResultsCount(w io.Writer, results []StepResult, format string) int

ReportStepResultsCount renders results per format via ReportStepResults and returns how many results ended in a FAIL verdict (no infra/check split — a caller needing that split uses ClassifyStepFailures directly). Moved from charly/check_feature_run.go's reportSteps+stepFailCount (CHECK-wave) — that pair had DRIFTED from this file's own comment above (which already claimed "both [check_cmd.go and check_feature_run.go] now delegate here"): check_feature_run.go's reportSteps still re-implemented the same format switch instead of calling ReportStepResults. Both callers already imported kit and had zero core-state coupling — a plain report+count wrapper.

func ResolveApkPath added in v0.2026202.1112

func ResolveApkPath(ref, candyDir string) (string, error)

ResolveApkPath resolves a committed-APK reference against the candy's SOURCE tree. Absolute paths are used verbatim; a relative path anchors candy-dir-relative first, then each ancestor up to the candy's project / repo root (first existing match wins). This resolves a path like `tests/data/ApiDemos-debug.apk` identically whether the candy is LOCAL (candyDir under the consuming project root) or fetched via @github (candyDir under the cloned-repo cache, where a project-root-relative file lives at <repo-root>/tests/data/... several levels above candyDir).

It FAILS HARD when a relative ref has no candy dir to anchor against, or when the file is not found anywhere up the tree — the caller surfaces that, never silently passing an unresolvable path downstream.

func ResolveAutoEnable added in v0.2026190.848

func ResolveAutoEnable(envVal string, cfgVal *bool) bool

func ResolveBoxName added in v0.2026198.345

func ResolveBoxName(box string) string

ResolveBoxName extracts the short box name from a ref that may be a local box name or a remote ref (github.com/org/repo/box[@version]).

func ResolveCheckLevel added in v0.2026191.1837

func ResolveCheckLevel(level string) string

ResolveCheckLevel normalizes an authored check_level to a canonical rung, applying the default for the empty value. An unknown value is returned verbatim so the validator flags it — never silently defaulted.

func ResolveDeployPorts added in v0.2026190.848

func ResolveDeployPorts(containerPorts []int, pins, prior []string, occupied map[int]bool) ([]string, error)

ResolveDeployPorts maps each image-declared container port to a host:container publish mapping — the AUTO-PORT-MAPPING default. For every container port:

  • an explicit deploy pin (host:container, matched by container port) wins;
  • else a still-valid prior allocation (from a previous `charly config` — keeps a deploy's host ports STABLE across `charly update`) is reused;
  • else a fresh free 127.0.0.1 host port is allocated.

`occupied` seeds host ports already taken by SIBLING deployments so concurrent beds never collide; every chosen host port is recorded back into it. Pins for container ports the image does not expose are honored too (an operator publishing an extra port). A stray `auto` token in `pins` is ignored (treated as "no pin" → allocate), so a not-yet-migrated `port: [auto]` still works. Returned mappings carry no bind address; localizePort prepends BindAddress (127.0.0.1 by default) at quadlet/run time so every published port is loopback.

func ResolveEncryptedStoragePath added in v0.2026190.848

func ResolveEncryptedStoragePath(envVal, cfgVal string) string

func ResolveEntrypointFromMeta added in v0.2026198.345

func ResolveEntrypointFromMeta(meta *spec.BoxMetadata) []string

ResolveEntrypointFromMeta determines the entrypoint from image metadata (runtime mode). Label-first: the build-resolved init contract is baked into the ai.opencharly.init_def label (meta.InitDef), so any init system declared in the embedded `init:` vocabulary — including custom ones — now reaches runtime. wellKnownInitDefs is consulted only for pre-init_def-label images (built before the label existed; their labels cannot be re-baked).

func ResolveEnvVars added in v0.2026190.848

func ResolveEnvVars(globalEnv []string, deployEnv []string, deployEnvFile string, envDir string, cliEnvFile string, cliEnv []string) ([]string, error)

ResolveEnvVars merges env vars from multiple sources. Priority (last wins for duplicate keys): global env < deploy config < workspace .env < CLI --env-file < CLI -e flags.

func ResolveInitDefFromMeta added in v0.2026198.345

func ResolveInitDefFromMeta(meta *spec.BoxMetadata) (*spec.ResolvedInit, error)

ResolveInitDefFromMeta returns the init contract for management-command rendering. Label-first: the build-resolved def is baked into the ai.opencharly.init_def label, so any vocabulary-declared init system — including custom ones — resolves at runtime. Falls back to wellKnownInitDefs only for pre-init_def-label images (built before the label existed).

func ResolveLocalImageRef added in v0.2026197.1320

func ResolveLocalImageRef(engine, input string) (string, error)

ResolveLocalImageRef resolves a user-supplied image reference against the engine's local storage — never reads charly.yml. Used by test-mode commands (charly check live, charly check box) so they stay within the test-mode input set.

For full refs (registry prefix present) it validates the image exists locally and passes through unchanged. For short names it resolves via CalVer: collect every local image matching the short name (either by `ai.opencharly.image=<short>` label or by the tag-suffix short-name match) and pick the one whose tag has the highest CalVer (or the highest `ai.opencharly.version` label). charly is CalVer-only — no `:latest` fallback. See `/charly-build:build` "CalVer-only" for the contract.

Returns `ErrImageNotLocal` when nothing matches. An ambiguous result across multiple repos with the same highest CalVer tag surfaces as an explicit error asking for a full ref.

func ResolveNetwork added in v0.2026198.345

func ResolveNetwork(configured, engine string) (string, error)

ResolveNetwork returns the network to use for a container. If configured is non-empty (explicit override like "host"), it is returned as-is. Otherwise, the shared "charly" network is ensured and returned.

func ResolveNewestLocalCalVer added in v0.2026197.1320

func ResolveNewestLocalCalVer(engine, short string) (string, error)

ResolveNewestLocalCalVer is the canonical "find the newest local image for this short name" helper. Thin wrapper around ResolveLocalImageRef — exposed so callers that start with an explicit short-name + empty-tag can resolve uniformly.

func ResolvePackageName

func ResolvePackageName(pkg string, packageMap map[string]string, distros []string) string

ResolvePackageName picks the correct package name for the running image's distro: if packageMap has a key matching any of the image's distro tags (first match wins — tags are authored most-specific-first, "fedora:43" before "fedora"), that mapping is used; otherwise the bare pkg name. The single cross-distro name resolver shared by the `package` candy's check + act AND the host's step materializer (R3).

func ResolveProto added in v0.2026198.345

func ResolveProto(containerPort int, portProtos map[string]string) string

ResolveProto returns the backend scheme for a container port, defaulting to "http". portProtos is string-keyed (the OCI-label wire form, P2B reshape) — index by the port as a string.

func ResolveShellImageRef added in v0.2026198.718

func ResolveShellImageRef(registry, name, tag string) string

ResolveShellImageRef builds the full image reference from registry, name, and tag, for a caller about to run an engine command (podman/docker) against it. When tag is empty, it resolves to the newest local CalVer for the given short name via ResolveNewestLocalCalVer — the CalVer-only contract (`/charly-build:build` "Cache Efficiency"). A caller that wants a specific tag passes it; a caller whose `--tag` flag is empty gets the newest CalVer with no extra work. When registry is set AND tag is empty, there's no way to guess a remote CalVer without a registry-list call, so the caller gets `<registry>/<name>` back with no tag suffix — the engine resolves it locally first (matching any single local tag) or errors. (P14: relocated from charly/shell.go's resolveShellImageRef, which now delegates here — R3, single source for every caller: candy/plugin-box's `merge` command plus charly core's own bundle_add/config_image/ensure_image/remote_image/pod_lifecycle_resolve/update_deploy_dispatch.)

func ResolveValue added in v0.2026190.848

func ResolveValue(envVal, cfgVal, defaultVal string) string

ResolveValue returns the first non-empty value from the chain.

func ResolveVmSshPort added in v0.2026201.1934

func ResolveVmSshPort(vm *spec.ResolvedVm, vmName string, persistedPort int) (int, error)

ResolveVmSshPort resolves the guest SSH host port from the resolved spec:

  • ssh.port_auto: true → reuse persistedPort when the caller already resolved one (idempotent across rebuilds), else allocate a free host port.
  • ssh.port: N → that fixed port.
  • neither → 2222.

The persisted-state READ is the caller's concern (it differs by placement: charly core reads its project config directly; an out-of-process plugin reads over a host seam) — this is the PURE resolution/allocation decision only.

func ResolveVolumesPath added in v0.2026190.848

func ResolveVolumesPath(envVal, cfgVal string) string

func ResugarPlan added in v0.2026198.345

func ResugarPlan(plan *yaml.Node)

ResugarPlan is the desugar's INVERSE, used by the deploy-state WRITER (sdk/deploykit's marshalBundleNode): each step's internal plugin/plugin_input pair rewrites back to the authored `<word>: <input>` sugar (collapsing a single-primary map to the scalar shorthand), so a written file round-trips through the parse-time desugar instead of tripping its authored-envelope ban.

func RunAgentOnce added in v0.2026197.1320

func RunAgentOnce(ctx context.Context, ai *spec.AgentExecSpec, prompt string, timeout time.Duration) (string, string, error)

RunAgentOnce launches the configured AI CLI exactly once with the given prompt and returns its stdout/stderr. It is the bounded, single-shot sibling of the harness loop's iteration launcher (candy/plugin-check/harness_loop.go) and of LocalCaptureVersion (candy/plugin-check/agent.go) — same host-exec shape, no iteration directories, no plateau state. ${PROMPT} in the AI's command argv (and a PromptVia: file temp file) is substituted with the prompt text.

func RunCaptureCmd added in v0.2026190.848

func RunCaptureCmd(cmd *exec.Cmd) (string, string, int, error)

func RunReverseOps added in v0.2026190.848

func RunReverseOps(ops []ReverseOp, exec ReverseExecutor)

RunReverseOps executes ops in REVERSE order (last-installed, first- removed). Idempotent where possible: a missing file is treated as "already removed" rather than an error.

func SameStringSlice added in v0.2026190.848

func SameStringSlice(a, b []string) bool

SameStringSlice reports whether two string slices are element-wise equal (order-sensitive) — used to skip a redundant resolved-port re-save.

func SanitizeDeployName added in v0.2026197.1320

func SanitizeDeployName(s string) string

func SaveRuntimeConfig added in v0.2026190.848

func SaveRuntimeConfig(cfg *RuntimeConfig) error

SaveRuntimeConfig writes the runtime config file, creating directories as needed.

func SaveYAMLNodeFile added in v0.2026193.1241

func SaveYAMLNodeFile(path string, root *yaml.Node) error

SaveYAMLNodeFile marshals a node tree back to an arbitrary file path, preserving comments + key order (the yaml.v3 Node round-trip). Shared by the scaffold/authoring engine (kit.AddBox) and charly core's box add-candy/rm-candy verbs — ONE marshal-to-file home (R3).

func ScaffoldCandy added in v0.2026193.1241

func ScaffoldCandy(dir, name, calver string) error

ScaffoldCandy creates a new candy directory at dir/<DefaultCandyDir>/<name> with a placeholder manifest in the compact name-first node form. ADE mandates a description + at least one deterministic check: step, so the scaffold ships a minimal passing pair the author replaces. calver stamps the candy's mandatory version:. Errors if the candy already exists. The caller prints the created path.

func ScaffoldProject added in v0.2026193.1241

func ScaffoldProject(dir string) error

ScaffoldProject creates an empty charly project at dir. Idempotency: errors out if dir already contains an charly.yml so we never silently clobber an existing project. The dir itself may exist. The seed's schema version is stamped to the current HEAD (LatestSchemaVersion).

func ServiceName added in v0.2026201.1135

func ServiceName(boxName string) string

ServiceName returns the systemd service name for an image.

func ServiceNameInstance added in v0.2026201.1135

func ServiceNameInstance(boxName, instance string) string

ServiceNameInstance returns the systemd service name for an image with optional instance.

func SetByDotPath added in v0.2026186.1019

func SetByDotPath(path, dotpath, valueYAML string) error

SetByDotPath edits the file at path, navigating into the YAML structure via dotpath (dot-separated keys) and replacing the leaf value with valueYAML (parsed as YAML so callers can pass scalars, lists, or maps). Comments and key order are preserved.

func ShDoubleQuote

func ShDoubleQuote(v string) string

ShDoubleQuote wraps a string in double quotes for a shell context where variable expansion MUST still happen (e.g. download URLs that template ${BUILD_ARCH}). Escapes the metachars that break out of a double-quoted string, but deliberately does NOT escape `$` so authored ${FOO} / $FOO still expand.

func ShDoubleQuotePath

func ShDoubleQuotePath(v string) string

ShDoubleQuotePath escapes a PATH-list value for use INSIDE double quotes, leaving `$` unescaped so `$PATH` expands at sourcing time.

func ShQuoteArg

func ShQuoteArg(v string) string

ShQuoteArg single-quotes an argument for POSIX shell embedding.

func ShQuoteEnv

func ShQuoteEnv(v string) string

ShQuoteEnv single-quotes a value for POSIX sh (env.d export values). Inside single quotes nothing needs escaping except the single quote itself.

func ShellInitFilePath

func ShellInitFilePath(shell ShellKind, home string) string

ShellInitFilePath returns the init file the managed block lands in for each shell under a home.

func ShellQuote

func ShellQuote(s string) string

ShellQuote wraps s in single quotes for safe interpolation into a shell command (the importable analogue of charly's shellSingleQuote). Embedded single quotes are escaped as '\”.

func ShutdownProcessGroup added in v0.2026200.1312

func ShutdownProcessGroup(cmd *exec.Cmd, stdin io.Closer, done <-chan struct{})

ShutdownProcessGroup stops one Setpgid child process gracefully-first and reaps it. The escalation ladder is:

  1. close stdin — the polite EOF signal every stdio-carried protocol answers by exiting on its own;
  2. one ProcessShutdownGrace for the process to exit;
  3. SIGTERM to the whole process group — the explicit termination request, so a well-behaved child can still shut down cleanly;
  4. a second ProcessShutdownGrace;
  5. only then SIGKILL to the group — the final escalation a child cannot ignore — and reap.

The ladder never opens with a force-kill: SIGKILL orphans nothing (the whole group dies), but it also gives no child the chance to flush protocol state, so it stays the last resort.

done must be closed by the caller's cmd.Wait() monitor once the process is reaped; ShutdownProcessGroup returns only after that, so no zombie or running group member survives the call. Call it once per process — wrap in sync.Once when several paths may close the same process.

func SidecarConfigDir added in v0.2026198.358

func SidecarConfigDir() (string, error)

SidecarConfigDir returns the per-user directory where sidecar companion config files live (e.g. charly-foo-tailscale-serve.json), used by the `charly config remove` sidecar-config sweep.

func SidecarContainerName added in v0.2026198.345

func SidecarContainerName(boxName, sidecarName string) string

SidecarContainerName returns the container name for a named sidecar.

func SidecarContainerNameInstance added in v0.2026198.345

func SidecarContainerNameInstance(boxName, instance, sidecarName string) string

SidecarContainerNameInstance returns the container name for a named sidecar, instance-aware.

func SidecarQuadletFilename added in v0.2026201.1135

func SidecarQuadletFilename(boxName, sidecarName string) string

SidecarQuadletFilename returns the quadlet filename for a sidecar container.

func SidecarQuadletFilenameInstance added in v0.2026201.1135

func SidecarQuadletFilenameInstance(boxName, instance, sidecarName string) string

SidecarQuadletFilenameInstance returns the quadlet filename for a sidecar with optional instance.

func SortStrings

func SortStrings(s []string)

SortStrings sorts s in place (ascending). A small insertion-free bubble sort, kept identical to the original package-main helper.

func SortedEnvPairs added in v0.2026200.1312

func SortedEnvPairs(env map[string]string) []string

SortedEnvPairs renders an environment map as a deterministically ordered "KEY=value" slice. Sorting makes launched-process environments and rendered remote commands byte-reproducible across runs (map iteration order is not).

func SplitHostKey added in v0.2026197.1537

func SplitHostKey(key string) (name, arg string, ok bool)

SplitHostKey splits a "HOST:web" / "HOST:web:8080" key into the variable name and the remaining argument(s) (everything after the FIRST colon).

func SshConfigPath added in v0.2026186.739

func SshConfigPath(home string) string

SshConfigPath returns ~/.ssh/config for the user's home dir.

func SshFragmentPath added in v0.2026186.739

func SshFragmentPath(home string) string

SshFragmentPath returns ~/.config/charly/ssh_config for the user's home dir.

func StampDescent added in v0.2026187.1533

func StampDescent(node *spec.Deploy, traitsFor func(word string) *spec.DeployTraits)

StampDescent stamps node.Descent from the substrate's DECLARED traits (resolved by `traitsFor(node.Target)`) and recurses into the whole nested (Children) + peer (Members) subtree, so every node the deploy chain can descend into carries a descriptor. Idempotent — re-stamping with the same traitsFor writes the same value. traitsFor must never be nil; it returns nil for a word with no declared substrate traits (a targetless group / empty target), which DescentFromTraits maps to the external-in-place default.

func StepDoMode added in v0.2026191.2205

func StepDoMode(s *spec.Step) spec.DoMode

StepDoMode maps the step keyword to the act/assert/instruct dispatch enum.

func StepID added in v0.2026191.2205

func StepID(origin string, stepIdx int) string

StepID returns the stable identifier used for plan-overlay merge lookups, depends_on references, and ${STEP_ID} substitution — a deterministic id derived from origin + position.

func StripManagedBlock

func StripManagedBlock(existing, marker string) string

StripManagedBlock removes the begin/end fence pair (tagged with marker) and its body.

func StripPortSuffix added in v0.2026190.848

func StripPortSuffix(s string) (string, string)

StripPortSuffix removes /tcp or /udp protocol suffix from a port string. "47998/udp" -> "47998", "udp"; "8000" -> "8000", ""

func StripURLScheme added in v0.2026198.345

func StripURLScheme(ref string) string

StripURLScheme removes http:// or https:// from a remote ref if present.

func SystemdUserAvailable added in v0.2026190.848

func SystemdUserAvailable() bool

SystemdUserAvailable reports whether a functional `systemd --user` session is reachable for the current process. Both signals must hold:

  • $XDG_RUNTIME_DIR is set (the bus address resolves against it)
  • /run/user/<uid>/systemd exists as a directory (systemd-user has populated its runtime dir, i.e. the user-instance has actually started)

Either alone is insufficient: $XDG_RUNTIME_DIR can be set in stale environments where systemd-user never came up, and the runtime dir can exist on systems where the env var got dropped (sudo without -E, container entrypoints).

func SystemdUserDir added in v0.2026201.1135

func SystemdUserDir() (string, error)

SystemdUserDir returns the user-level systemd unit directory (~/.config/systemd/user/).

func TaskShellPreamble

func TaskShellPreamble(candyVars, opEnv map[string]string) string

TaskShellPreamble returns the BUILD_ARCH/ARCH exports plus any candy vars + op env (sorted for deterministic output) so cmd: bodies can reference ${ARCH} / ${MY_CANDY_VAR} at deploy-time the same way they do at build-time.

func TestVarRefs added in v0.2026191.2033

func TestVarRefs(s string) []string

TestVarRefs returns the set of ${NAME[:arg]} references in s, as their fully-qualified keys (matching the env-map format used by ExpandTestVars). Used by the validator to catch typos at config time.

func TransferImage added in v0.2026190.848

func TransferImage(srcEngine, dstEngine, imageRef string) error

TransferImage pipes an image from one engine to another via save | load.

func TransferToRootful added in v0.2026190.848

func TransferToRootful(imageRef string) error

TransferToRootful pipes an image from rootless podman storage into rootful (sudo podman) storage via `podman save | sudo podman load`. Idempotent — returns nil immediately when the image already exists in rootful storage.

Used by RunPrivileged when engine.rootful=sudo because rootless and rootful podman maintain separate container-storage trees (~/.local/share/containers vs /var/lib/containers). Without this transfer, sudo podman run against a locally-built image falls back to a registry pull (which 403s for build-only images that were never pushed).

Surfaced 2026-05 by the cachyos / cachyos-pacstrap-builder pair — the first time the bootstrap-builder framework was exercised end-to-end on a host with rootless build + sudo run.

func TrimPreview

func TrimPreview(s string) string

TrimPreview truncates s to a 200-char preview (trailing "…") for compact check-output display.

func TunnelConfigFromMetadata added in v0.2026198.345

func TunnelConfigFromMetadata(meta *spec.BoxMetadata) *spec.TunnelConfig

TunnelConfigFromMetadata creates a TunnelConfig from image label metadata. Unlike ResolveTunnelConfig, this doesn't need candy access since the tunnel configuration is already stored in the label.

func UnixToTCPBridge added in v0.2026197.1537

func UnixToTCPBridge(socketPath string) (net.Listener, error)

UnixToTCPBridge starts a TCP listener on 127.0.0.1:0 that pipes each accepted connection to the named UNIX socket. The returned listener owns a goroutine that exits when the listener is closed. Used wherever an RFB client (or any TCP-only peer) must reach a UNIX-socket-only service.

func UserScopeBindMounts added in v0.2026190.848

func UserScopeBindMounts(hostHome string) (map[string]string, error)

UserScopeBindMounts returns the standard subdirs each user-scope builder needs bind-mounted: $HOME/.pixi, $HOME/.cargo, $HOME/.npm-global, $HOME/.cache/charly. Ensures the source dirs exist on the host (empty dirs — so podman's --mount doesn't reject them).

func UserScopeEnv added in v0.2026190.848

func UserScopeEnv(hostHome string) map[string]string

UserScopeEnv returns the env-var set that pixi/npm/cargo need to write into the bind-mounted directories rather than the builder's own home paths.

func ValidateBindAddress added in v0.2026190.848

func ValidateBindAddress(value string) error

func ValidateEngine added in v0.2026190.848

func ValidateEngine(value, field string) error

func ValidatePlanSteps added in v0.2026197.1320

func ValidatePlanSteps(desc string, plan []spec.Step, eid string) []string

planvalidate.go — ValidatePlanSteps, the SHARED static plan-block validator (P12a: relocated from charly/plan_validate.go). It lives here — not candy/plugin-check — because it is invoked by BOTH `charly box validate` (charly/validate.go) AND the externalized `charly feature` command's "feature" HostBuild seam (charly/host_build_feature.go): CORE calls it directly at both sites, so it must be reachable without importing a plugin candy. One copy, R3.

  • description non-empty
  • every step has exactly one keyword (StepKind())
  • run/check steps carry exactly one Op verb; agent-* steps carry none

Returns a list of human-readable error strings (empty if OK).

func ValidateRunMode added in v0.2026190.848

func ValidateRunMode(value string) error

func ValidateTagExpr added in v0.2026201.1934

func ValidateTagExpr(tag string) error

ValidateTagExpr syntax-checks an optional --tag expression (rejecting a malformed one) without keeping the parsed *TagExpr — moved from charly/check_feature_run.go (CHECK-wave), a pure wrapper with zero core-state coupling. It does NOT apply the parsed expression as a step filter: kit.RunPlan (the walk both hostFeatureBox and hostFeatureLive drive) takes no tag-filter parameter and walks every step unconditionally — a confirmed, RCA'd, non-blocking gap (per-tag filtering was never wired past this parse), routed to the next check-correctness thematic batch.

func VenueFromDescriptor added in v0.2026203.438

func VenueFromDescriptor(d spec.VenueDescriptor) (spec.DeployExecutor, error)

VenueFromDescriptor re-materializes a spec.VenueDescriptor into a real DeployExecutor — the decouple point that lets a substrate lifecycle plugin run out-of-process: it hands back a serializable venue description over the wire, and whichever side needs a LIVE executor (previously only the host, via charly/substrate_lifecycle_grpc.go's now-deleted venueFromDescriptor) re-materializes it locally. Promoted here (S3b, Unit-6 design) because the function has ZERO core-only dependency (pure sdk/kit + sdk/spec) and now has TWO callers that must construct the byte-identical executor from the same descriptor: the host (still re-materializing it for --verify / subsequent dispatch calls on the same target) and candy/plugin-bundle (materializing its OWN executor for the substrate's Execute call, immediately after driving that same substrate's OpPrepareVenue). R3 — one function, not a duplicate in each package.

func VmSshAlias added in v0.2026186.739

func VmSshAlias(vmName string) string

VmSshAlias returns the canonical alias for a VM deployment name ("charly-" namespaced).

func WaitForCloudInit added in v0.2026186.739

func WaitForCloudInit(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForCloudInit polls `sudo cloud-init status` (as root — charly guests have passwordless sudo) until cloud-init reaches an EXPLICIT terminal status (done/error/disabled) over a SURVIVING ssh connection — that is the deterministic signal that first-boot host-key regen finished (sshd stable) AND the seed-package phase (which holds the distro package lock) completed. Ready ONLY on a terminal token: a blank/transient/"running" read keeps polling, so the deploy's first `pacman -Sy` can't race cloud-final.service. Only meaningful for cloud-image sources; skip for bootc with no cidata ISO.

func WaitForPackageLock added in v0.2026186.739

func WaitForPackageLock(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForPackageLock makes the guest package manager READY for the deploy's own pacman/apt/dnf: (1) block until no boot-time package PROCESS runs (the direct R4 sync primitive on the contended lock, process-based so a stale lock can't hang it forever), then (2) clear a STALE lock FILE that a reaped boot-time install left with no holding process (re-checking no-process inside the command keeps the removal safe).

func WaitForSSH added in v0.2026186.739

func WaitForSSH(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForSSH polls `ssh <alias> true` until sshd answers (BINARY/EDGE readiness: refused→up), under the injected poll's bounds. ConnectTimeout=2 bounds each connect; ServerAlive* bound a connected-then-blackholed session; the injected poll's per-attempt context is the never-hang bound.

func WalkPlans

func WalkPlans(ctx context.Context, exec DeployExecutor, plans []spec.InstallPlanView, opts WalkOpts) ([]spec.ReverseOp, error)

WalkPlans executes every plan's steps on the venue and returns the combined teardown ops (plugin-renderable kinds echo the host-computed view.ReverseOps; host-engine kinds return theirs from RunHostStep). The caller folds them into its DeployReply.

func WrapContainerCommand

func WrapContainerCommand(script string) string

WrapContainerCommand guards an in-container command-check script against stdin-consuming subcommands. The runner delivers in-container scripts to the pod shell over a stdin heredoc ("stdin-attached exec"); without this guard the FIRST subcommand that reads stdin — adb shell, ssh, read, cat — consumes the REST of the heredoc (the not-yet-executed script lines), silently truncating the check to its first command. Wrapping the whole script in a brace group with stdin redirected from /dev/null fixes it generically: the shell reads the entire group before executing it (so the heredoc is fully drained by parse time), then runs every subcommand with stdin tied to /dev/null. The host path (a plain `sh -c` argv) is unaffected.

func WriteCandyRecord added in v0.2026190.848

func WriteCandyRecord(paths *LedgerPaths, rec *CandyRecord) error

WriteCandyRecord serializes rec to candy/<candy>.json.

func WriteDeployRecord added in v0.2026190.848

func WriteDeployRecord(paths *LedgerPaths, rec *DeployRecord) error

func WriteDeployRecordVia added in v0.2026190.848

func WriteDeployRecordVia(exec spec.DeployExecutor, paths *LedgerPaths, rec *DeployRecord) error

WriteDeployRecordVia is the executor-routed variant of WriteDeployRecord. Same semantics as AddCandyDeploymentVia but for deploy records (deploys/<id>.json).

func WriteVmSshStanza added in v0.2026186.739

func WriteVmSshStanza(home string, s VmSshStanza) error

WriteVmSshStanza adds (or replaces) a Host stanza in the managed fragment. Idempotent.

Types

type AgentForwardMounts added in v0.2026198.345

type AgentForwardMounts struct {
	Volumes []string // host:container[:options] — only for container CREATION
	Env     []string // KEY=VALUE — for both creation and exec
}

AgentForwardMounts holds the resolved bind mounts and env vars needed to forward SSH and GPG agent sockets from the host into a container.

func ResolveAgentForwarding added in v0.2026198.345

func ResolveAgentForwarding(rt *ResolvedRuntime, deploy *spec.BundleNode, containerHome string) AgentForwardMounts

ResolveAgentForwarding detects available agent sockets on the host and returns the bind mounts and environment variables needed to forward them into a container. deploy may be nil (no per-image overrides). containerHome is the home directory inside the container (e.g., "/root" or "/home/user") — determines where GPG expects its agent socket.

Graceful degradation: logs warnings to stderr for missing sockets but never returns errors — missing agents are silently skipped.

type AgentGrader added in v0.2026197.1320

type AgentGrader struct {
	Agent    *spec.AgentExecSpec // the resolved kind:agent exec spec (how to launch the CLI)
	Target   string              // the deployment name the agent probes (e.g. "check-pod")
	Instance string              // optional deploy instance
	Timeout  string              // optional Go-duration override (from --timeout)
}

AgentGrader is the production StepGrader: it drives the configured `kind: agent` CLI against a live deployment.

func (*AgentGrader) Grade added in v0.2026197.1320

Grade builds the grader prompt, runs the AI once, and parses its verdict.

type BuilderRunOpts added in v0.2026190.848

type BuilderRunOpts = spec.BuilderRunOpts

type CalVer

type CalVer struct {
	Year int // calendar year (e.g. 2026)
	Day  int // day of year, 1-366
	HHMM int // hour*100 + minute, 0-2359
}

CalVer is a parsed YYYY.DDD.HHMM calendar version. The same format that ComputeCalVer (core) emits for image tags is, since the 2026-05 schema-versioning cutover, the schema-version stamp carried by every versioned YAML config. The declarative migration table is ordered by CalVer, and the load-time gate compares a file's CalVer against LatestSchemaVersion.

func LatestSchemaVersion

func LatestSchemaVersion() CalVer

LatestSchemaVersion is the HEAD schema CalVer — every current-format versioned file is stamped to it and the load-time gate requires it. Core exposes it via a thin shim of the same name; the in-core migration engine reads it directly.

func MustCalVer

func MustCalVer(s string) CalVer

MustCalVer parses a compile-time-constant CalVer literal, panicking on a malformed value. Used for the CUE-owned HEAD/floor consts (spec.SchemaVersion / spec.SchemaFloor), so a non-canonical literal that slipped past the strict #CanonCalVer CUE gate still fails fast at process start rather than silently mis-ordering the migration table.

func ParseCalVer

func ParseCalVer(s string) (CalVer, bool)

ParseCalVer parses the CANONICAL CalVer string "YYYY.DDD.HHMM" — exactly a 4-digit year, a 3-digit zero-padded day-of-year, and a 4-digit zero-padded HHMM, separated by dots. It is EXTREMELY STRICT and has NO backward compatibility: every component must be the exact width, pure ASCII digits (no sign, no inner whitespace), within range (day 1-366, hour 0-23, minute 0-59). Anything else — the legacy integer "4", a non-padded "2026.45.830", an empty string, junk — returns ok=false. (Surrounding whitespace, a transport artifact of e.g. a `charly version` trailing newline, is trimmed before the format check.)

A false result is exactly what the schema gate and migration runner treat as "older than every real CalVer", so a non-canonical config flows into `charly migrate` and is re-stamped canonical — one clean migration forward.

Because the canonical form is fixed-width zero-padded, a plain alphanumeric (lexicographic) sort of CalVer strings is chronological (see CalVer.Less).

func SchemaFloor

func SchemaFloor() CalVer

SchemaFloor is the oldest schema CalVer `charly migrate` can migrate FROM. A config below it (or with a non-CalVer version) is unmigratable — the engine refuses it with an actionable "predates the supported floor" error rather than blind-stamping a stale body to HEAD.

func (CalVer) Less

func (c CalVer) Less(o CalVer) bool

Less reports whether c is chronologically before o. Because the canonical string form is fixed-width zero-padded, chronological order IS lexicographic order, so this is a plain string comparison.

func (CalVer) String

func (c CalVer) String() string

String renders the canonical CalVer "YYYY.DDD.HHMM" — 4-digit year, 3-digit zero-padded day, 4-digit zero-padded HHMM. This is the ONLY form ParseCalVer accepts, so String∘Parse is the identity and a plain alphanumeric sort of these strings is chronological.

type CandyRecord added in v0.2026190.848

type CandyRecord struct {
	SchemaVersion string       `json:"schema_version,omitempty"`
	Candy         string       `json:"candy"`
	Version       string       `json:"version,omitempty"`
	DeployedBy    []string     `json:"deployed_by"` // set of deploy IDs
	DeployedAt    string       `json:"deployed_at"`
	BuilderImage  string       `json:"builder_image,omitempty"`
	Steps         []StepRecord `json:"steps,omitempty"`       // completed steps, in install order
	ReverseOps    []ReverseOp  `json:"reverse_ops,omitempty"` // precomputed ops for teardown
}

CandyRecord is the per-candy ledger entry. Lists concrete artifacts (packages installed, files written, services enabled, env.d file created, repo changes) so reversal doesn't need to re-compile the plan from the candy manifest.

func ReadCandyRecord added in v0.2026190.848

func ReadCandyRecord(paths *LedgerPaths, layer string) (*CandyRecord, error)

ReadCandyRecord loads candy/<candy>.json; returns nil, nil if absent.

func RemoveCandyDeployment added in v0.2026190.848

func RemoveCandyDeployment(paths *LedgerPaths, candyName, deployID string) (*CandyRecord, bool, error)

RemoveCandyDeployment decrements a candy's deployed_by set. Returns (recordAfter, shouldFullyRemove, error). When shouldFullyRemove is true, the caller should perform the actual file/package/service teardown and then delete the candy ledger entry.

type CharlyInstallStrategy added in v0.2026186.739

type CharlyInstallStrategy string

CharlyInstallStrategy is the resolved charly_install.strategy: "auto" | "scp" | "skip".

const (
	CharlyInstallAuto CharlyInstallStrategy = "auto"
	CharlyInstallScp  CharlyInstallStrategy = "scp"
	CharlyInstallSkip CharlyInstallStrategy = "skip"
)

func ResolveCharlyInstallStrategy added in v0.2026186.739

func ResolveCharlyInstallStrategy(raw string) CharlyInstallStrategy

ResolveCharlyInstallStrategy applies the default to a raw charly_install.strategy string.

type CheckContext

type CheckContext interface {
	// Exec runs commands on the venue (in-container under ModeLive, in a disposable
	// container under ModeBox, or host-side depending on the executor).
	Exec() Executor
	// Mode is the run mode (Live vs Box).
	Mode() RunMode
	// HTTPDo issues an HTTP request from the CHARLY HOST's network namespace, applying
	// the per-request TLS / redirect / CA policy in req, and returns the status, body, and
	// response headers. It REPLACES the former HTTPClient() *http.Client leg: an
	// *http.Client cannot cross a process boundary, so out-of-process the REQUEST crosses
	// (CheckContextService.HTTPDo) and the host dials; in-process the host builds the client
	// and dials directly. The transport-level error is returned as err (a non-2xx is NOT an
	// error — the caller matches resp.Status).
	HTTPDo(ctx context.Context, req HTTPRequest) (HTTPResponse, error)
	// ResolveEndpoint resolves the check target's venue (container / VM / ssh / local) and
	// returns a host-reachable "host:port" address for an in-venue TCP port, opening (and
	// host-side tracking, for teardown after this verb's Invoke) any ssh -L forward a VM/ssh
	// venue needs. An endpoint verb (cdp/vnc/spice/…) declares its in-venue port and dials
	// the returned addr — REPLACING the per-verb host preresolvers: the host owns the
	// venue/podman/go-libvirt machinery the out-of-process plugin lacks. Empty addr with a
	// nil error means "no live venue" (box-mode / no-box) — the verb's own no-endpoint skip
	// then fires; a resolution failure is returned as err.
	ResolveEndpoint(ctx context.Context, port int) (addr string, err error)
	// ResolveGraphicsEndpoint resolves a VM's <graphics type='<kind>'> listener (kind =
	// "vnc" | "spice") to a dialable endpoint, opening (and host-side tracking, for teardown
	// after this verb's Invoke) any ssh -L forward + socket->TCP bridge the venue needs. A
	// graphics verb (vnc/spice) calls it instead of the removed per-verb host preresolver;
	// the host owns the go-libvirt resolution, tunnel, bridge, and credential-store password.
	// GraphicsEndpoint.Skip=true means the deployment declares no graphics device of that kind
	// (an N/A skip). A zero GraphicsEndpoint with a nil error means no live VM context.
	ResolveGraphicsEndpoint(ctx context.Context, kind string) (GraphicsEndpoint, error)
	// ResolveClusterContext maps a charly k8s cluster-profile name to its kubeconfig context by
	// reading the project's kind:k8s spec — the host owns the project loader the out-of-process
	// plugin cannot reach. A cluster-probing verb (kube/…) declares its cluster and gets the
	// context; an empty string (no matching profile) means "fall back to the current-context".
	ResolveClusterContext(ctx context.Context, cluster string) (kubeContext string, err error)
	// ResolveImageLabel reads one OCI label value off the deployment-under-test's image — the
	// host owns the podman engine + container→image resolution the out-of-process plugin cannot
	// reach. A verb declares the label it needs (mcp reads ai.opencharly.mcp_provide) and parses
	// the returned value; an empty string means the label is absent on the image.
	ResolveImageLabel(ctx context.Context, label string) (value string, err error)
	// DialTimeout is the per-dial ceiling for host-side TCP reachability probes.
	DialTimeout() time.Duration
	// Box / Instance are the deployment's image + instance names (empty under ModeBox).
	Box() string
	Instance() string
	// Distros is the image's distro tag list (e.g. ["fedora:43","fedora"]) for
	// distro-specific package-name resolution.
	Distros() []string
	// AddBackground registers a host-side background process PID with the active plan run
	// so plan teardown reaps it (SIGTERM). A no-op when the engine has no scenario context
	// (a bare-Op run) or pid<=0. Used by a verb that fire-and-forgets a host process
	// (the `command` verb's background path).
	AddBackground(pid int)
}

CheckContext is the live check-engine surface a host-coupled verb's RunVerb consumes. charly's *Runner implements it; a candy reaches the running deployment through it without importing charly's package main.

type CheckResult added in v0.2026191.1732

type CheckResult struct {
	spec.CheckResult

	// DeadlineExceeded marks a result whose probe was killed by hitting its OWN
	// per-attempt deadline (probeNeverHang), NOT an external infra interruption. The
	// group-kill in runCaptureCmd surfaces the deadline SIGKILL as a signal-kill, so
	// without this flag the killed-probe retry (probeWasKilled) would futilely re-run a
	// probe that will only re-hang and re-hit the same deadline. An authoritative
	// "too slow" failure — never retried. NEVER crosses the wire (see above).
	DeadlineExceeded bool `json:"-"`
}

CheckResult is the engine's record of running a single check: the verb's verdict (Status/Message/CapturedValue — the same three fields a verb returns as a Result) plus the engine bookkeeping a Result does not carry (which Op ran, its Verb, and the timing / retry accounting). The dispatch boundary builds a CheckResult from a verb's Result by stamping Op/Verb/Elapsed.

Attempts and TotalElapsed are populated only when the check had an `eventually:` modifier (retry loop): Attempts=1 + TotalElapsed==Elapsed for a check that ran exactly once. Reporters surface these when Attempts>1 so slow startup paths are visible ("PASS in 5 attempts over 12.3s").

FLOOR-SLIM Unit 4 — the wire-envelope split: every field above is now spec.CheckResult (CUE-sourced, sdk/schema/checkresult.cue), EMBEDDED here. The ONE exception is DeadlineExceeded (below) — the spike-proven (P12) exception the wire mandate's own documented exception path authorizes: `json:"-"` (keep-in-Go, drop-from-wire) has no gengotypes construct. charly core's registry-coupled floor files (provider.go, provider_verb.go, verb_builtins.go, unified_targets.go, provider_checkenv.go) reference spec.CheckResult DIRECTLY (they never touch DeadlineExceeded) — zero new sdk/kit import. Field-promotion means every existing `.Status`/`.Op`/`.Verb`/`.Message`/`.Elapsed`/ `.Attempts`/`.TotalElapsed`/`.CapturedValue` selector expression compiles UNCHANGED; only a composite LITERAL naming one of those fields must nest it under the embedded field name (`CheckResult{CheckResult: spec.CheckResult{Status: ...}, ...}`) — Go does not promote embedded-struct field names into a literal key position.

The wire keys are deliberately snake_case now (op/verb/status/message/elapsed/…), a documented breaking change to `--format json`/TAP output — see sdk/schema/checkresult.cue and CHANGELOG for the old→new key mapping.

func RunOne added in v0.2026191.2205

func RunOne(ctx context.Context, pc PlanContext, c *spec.Op) CheckResult

RunOne handles all the per-check housekeeping (verb resolution, skip handling, variable expansion, venue swap, do-mode routing) and dispatches to a verb handler through the VerbResolver seam. The `eventually:` retry wraps the verb dispatch when requested.

func RunWithEventually added in v0.2026191.1837

func RunWithEventually(ctx context.Context, check *spec.Op, handler func() CheckResult) CheckResult

RunWithEventually wraps a per-check verb handler in a retry loop when the check declares `eventually: <duration>`. The handler is called with no arguments — it closes over the check, runner, and context.

Semantics:

  • eventually: outer retry cap (parsed as time.Duration). Defaults the returned CheckResult to Attempts=1 when unset (handler runs exactly once, unchanged).
  • retry_interval: sleep between retries. Defaults to 1s. Must be ≤ eventually or the loop would sleep past the deadline on the first miss.
  • PASS semantics: the FIRST attempt that returns StatusPass wins; its stdout/captures/message are what propagate.
  • FAIL semantics: the LAST attempt before deadline is returned, ensuring authors see the most recent failure detail rather than a stale first-attempt error.
  • SKIP semantics: treated the same as FAIL for retry purposes — skips aren't actionable to retry against.

Variable expansion: the handler re-runs from the same expanded check each attempt. The caller must expand variables BEFORE calling RunWithEventually — re-expansion per attempt would re-evaluate ${CAPTURED:name} mid-run, which is unwanted (captures record on PASS only, and pre-pass attempts wouldn't see their own future capture).

Context: RunWithEventually honours ctx.Deadline / ctx.Done — a canceled context short-circuits the loop with the last attempt's result.

type CheckRunReply added in v0.2026194.1605

type CheckRunReply struct {
	Steps   []StepResult `json:"steps,omitempty"`
	Image   string       `json:"image,omitempty"`
	NoSteps bool         `json:"no_steps,omitempty"`
	// Header is the pre-formatted, kind-specific banner line the host builds ("Image: X
	// (container: Y)" for pod, "VM: <name> (ssh …)", "Local deploy: …", "Group bed: …") from
	// data only the host holds (container name, ssh user/host/port, member count), so the
	// plugin stays kind-blind: it prints Header, then the formatted Steps.
	Header string `json:"header,omitempty"`
	// Passthrough carries the one non-plan-run live path — a nested pod-in-VM leaf whose check
	// the host delegates to the guest over SSH (`charly check live <pod>` run INSIDE the guest),
	// whose stdout/stderr + exit code the plugin forwards verbatim. Nil for every plan-run mode.
	Passthrough *StepPass `json:"passthrough,omitempty"`
	// Score is the "score"-mode reply (P12 Wave-2): the AI-harness SCORING result — RunCheckLive's
	// per-step verdicts (the substituted, nonce-carrying scoring plan walked host-side) the plugin
	// scorer consumes (summary, StepByID, Classify). Nil for the box/live/feature plan-run modes,
	// which carry their verdicts in Steps. CUE-sourced (spec.CheckRunResults) so ONE definition
	// serves both the host and the relocated plugin scorer (SDD; no alias).
	Score *spec.CheckRunResults `json:"score,omitempty"`
}

CheckRunReply is the host-resolved result of a check-run. Steps is the per-step verdict list the plugin formats (FormatStepResults*) and tallies into an exit code. Image is the resolved image ref for the "Image: <ref>" header line. NoSteps signals the image declared no plan (the plugin prints "No plan steps defined for this image." and exits 0) — distinct from an empty Steps that ran zero scored steps. The host signals an infra error (bad image, engine failure) via the builder's error return, surfaced to the plugin.

type CheckVarResolver added in v0.2026197.1320

type CheckVarResolver struct {
	// Env is the final flat map: plain refs use the name as key, parameterized
	// refs use "name:arg" as key (e.g., "HOST_PORT:6379"). Matches the format
	// expected by ExpandTestVars.
	Env map[string]string
	// HasRuntime is true when the resolver was populated from a running
	// container. When false, runtime-only vars return unresolved (skip path).
	HasRuntime bool
}

CheckVarResolver holds the materials for variable expansion under `charly check live` (running container present) and `charly check box` (build-time, no container). Builders for each scope are ResolveCheckVarsRuntime and ResolveCheckVarsBuild.

func ResolveCheckVarsBuild added in v0.2026197.1320

func ResolveCheckVarsBuild(meta *spec.BoxMetadata) *CheckVarResolver

ResolveCheckVarsBuild builds a variable map for build-time tests (no running container). Only BoxMetadata-derived vars are populated.

func ResolveCheckVarsRuntime added in v0.2026197.1320

func ResolveCheckVarsRuntime(meta *spec.BoxMetadata, deploy *vmshared.BundleNode, engine, deployName, containerName, instance string) (*CheckVarResolver, error)

ResolveCheckVarsRuntime builds a variable map for `charly check live` against a running container. It combines image metadata (build-time knowledge), deploy overlay (per-host knowledge), and podman inspect (effective runtime state). Any of the inputs may be nil.

On inspect failure the function still returns a resolver with the build-time portion populated; HasRuntime is false and runtime-only vars will be unresolved downstream.

type CheckVerbProvider

type CheckVerbProvider interface {
	Reserved() string
	RunVerb(ctx context.Context, cc CheckContext, op *spec.Op) Result
}

CheckVerbProvider is the typed in-process contract a host-coupled check-verb candy implements. Reserved() is the verb word; RunVerb runs the probe against the live CheckContext and returns a Result. The authored plugin_input rides op.PluginInput (decode it into the candy's CUE-generated params struct).

type ContainerInspection added in v0.2026197.1320

type ContainerInspection struct {
	Name            string            `json:"Name"`
	Config          InspectConfig     `json:"Config"`
	HostConfig      InspectHostConfig `json:"HostConfig"`
	NetworkSettings InspectNetwork    `json:"NetworkSettings"`
	Mounts          []InspectMount    `json:"Mounts"`
}

ContainerInspection is the subset of `podman inspect` JSON that the test runner needs to resolve runtime ${…} variables. Field names mirror the podman/docker inspect schema — we read the JSON the engine produces, we do not construct it.

func (*ContainerInspection) IsHostNetworked added in v0.2026197.1320

func (c *ContainerInspection) IsHostNetworked() bool

IsHostNetworked returns true when the container uses --network=host, in which case every container port is trivially the same host port.

type DefaultDownloader added in v0.2026192.1026

type DefaultDownloader struct{}

DefaultDownloader is the built-in git fetch backend — it delegates to DownloadRepo (git clone into the cache). The host uses it until a refs plugin registers a different RefsDownloader. Stays in kit (real git-clone I/O, not a pure seam contract).

func (DefaultDownloader) Download added in v0.2026192.1026

func (DefaultDownloader) Download(repoPath, version string) (string, error)

Download implements RefsDownloader via the git DownloadRepo primitive.

type DeployExecutor

type DeployExecutor interface {
	// Venue returns the host executor's stable venue identifier.
	Venue(ctx context.Context) (string, error)
	// RunSystem runs a root (sudo) script on the venue; optsJSON is a marshalled EmitOpts (nil ok).
	RunSystem(ctx context.Context, script string, optsJSON []byte) error
	// RunUser runs an unprivileged script on the venue.
	RunUser(ctx context.Context, script string, optsJSON []byte) error
	// PutFile places content at a path on the venue (ownerRoot → root:root). Binary-safe.
	PutFile(ctx context.Context, remotePath string, content []byte, mode uint32, ownerRoot bool) error
	// GetFile reads a venue file back to the host (asRoot reads via sudo).
	GetFile(ctx context.Context, path string, asRoot bool) ([]byte, error)
	// RunCapture runs a command on the venue, returning stdout/stderr/exit separately.
	RunCapture(ctx context.Context, script string) (stdout, stderr string, exit int, err error)
	// RunHostStep drives a HOST-ENGINE step on the host engine + applies onto the venue,
	// returning the step's recorded reverse ops.
	RunHostStep(ctx context.Context, step spec.InstallStepView, optsJSON []byte) ([]spec.ReverseOp, error)
}

DeployExecutor is the reverse-channel surface WalkPlans drives. charly's plugin SDK *Executor satisfies it structurally (identical method set), so a plugin passes its sdk.Executor straight through — kit need not import the SDK (no cycle).

type DeployRecord added in v0.2026190.848

type DeployRecord struct {
	// SchemaVersion is the ledger-format version (the ledger-candy-keys
	// cutover's CalVer). Empty means a pre-cutover record (json:"layer"
	// keys) — the read path rejects it with a `charly migrate` hint.
	SchemaVersion string   `json:"schema_version,omitempty"`
	DeployID      string   `json:"deploy_id"`
	Image         string   `json:"image"`
	Tag           string   `json:"tag,omitempty"`
	Target        string   `json:"target"` // the deploy-record key, e.g. "vm:<name>" (only VM/local deploys write a DeployRecord)
	Candy         []string `json:"candy,omitempty"`
	AddCandy      []string `json:"add_candy,omitempty"`
	DeployedAt    string   `json:"deployed_at"`
}

DeployRecord is the top-level entry in deploys/<deploy-id>.json. Lists the image, tag, and the ordered candy set included in this deploy (image candies + add_candy overlays, already topo-sorted).

func ReadDeployRecord added in v0.2026190.848

func ReadDeployRecord(paths *LedgerPaths, id string) (*DeployRecord, error)

ReadDeployRecord loads deploys/<deploy-id>.json; returns nil, nil if the file doesn't exist.

type DiscoverConfig added in v0.2026194.1400

type DiscoverConfig []ScanSpec

DiscoverConfig is a FLAT list of generic scan specs. Each spec scans a path for directories containing its manifest; every discovered manifest is parsed as a multi-document stream and routed by SHAPE (the kind-key it carries), so one discover root can surface candies, boxes, deploys — any kind. There is no kind dimension and no hardcoded path/filename: discovery is fully configured in charly.yml.

type DocShape added in v0.2026194.1400

type DocShape = spec.DocShape

DocShape classifies one parsed YAML document's top level.

type EmitOpts added in v0.2026190.848

type EmitOpts = spec.EmitOpts

type EngineConfig added in v0.2026190.848

type EngineConfig struct {
	Build   string `yaml:"build,omitempty" json:"build,omitempty"`
	Run     string `yaml:"run,omitempty" json:"run,omitempty"`
	Rootful string `yaml:"rootful,omitempty" json:"rootful,omitempty"` // "auto", "machine", "sudo", "native"
}

EngineConfig specifies which container engine to use

type EnvConfig added in v0.2026190.848

type EnvConfig = spec.EnvConfig

EnvConfig — resolved candy env (KEY=value vars + PATH-append entries). CUE-SOURCED in spec now (sdk/schema/candymodel.cue, the S-CM enabler) so #CandyModel can compose it; this ALIASES onto spec (SDD). The helper functions below operate on it unchanged.

func ExpandEnvConfig added in v0.2026190.848

func ExpandEnvConfig(cfg *EnvConfig, home string) *EnvConfig

ExpandEnvConfig expands all ~ and $HOME references in an EnvConfig

func MergeEnvConfigs added in v0.2026190.848

func MergeEnvConfigs(configs []*EnvConfig) *EnvConfig

MergeEnvConfigs merges multiple env configs, later configs override earlier

type Executor

type Executor interface {
	// RunCapture runs a shell command/script on the venue, returning stdout,
	// stderr, the exit code, and any execution error (NOT a non-zero exit — that
	// is reported via the exit code). No root escalation; callers add sudo.
	RunCapture(ctx context.Context, script string) (stdout, stderr string, exit int, err error)
	// Kind classifies the venue: "host" | "container" | "image" | "vm".
	Kind() string
}

Executor is the subset of charly's DeployExecutor a check verb needs: run one command/script on the venue and capture stdout/stderr/exit separately. charly's DeployExecutor satisfies this structurally (RunCapture + Kind have identical signatures), so *Runner.Exec is passed straight through.

type FetchedImage added in v0.2026190.848

type FetchedImage struct {
	Path   string
	SHA256 string
}

FetchedImage is the result of FetchQcow2: the absolute path to the cached file plus the resolved sha256 (useful for logging / audit).

func FetchQcow2 added in v0.2026190.848

func FetchQcow2(src VmSource) (FetchedImage, error)

FetchQcow2 downloads (or reuses a cached copy of) a qcow2 URL, verifying the sha256 checksum. Resumable: partial downloads with a matching Content-Length are continued via Range: bytes=.

If the checksum value is empty, the fetcher attempts to auto-resolve a sidecar file at <url>.SHA256 / .sha256 / .sha256sum (Arch convention is .SHA256). The first one that returns HTTP 200 wins.

type GraderRequest added in v0.2026191.2205

type GraderRequest struct {
	Description string
	Keyword     string
	Text        string
	// ReadOnly is true for agent-check: (assessment only), false for agent-run: (may mutate).
	ReadOnly bool
}

GraderRequest is the agent grader's input for one agent-run:/agent-check: step.

type GraphicsEndpoint added in v0.2026187.2153

type GraphicsEndpoint struct {
	Addr        string
	Socket      string
	Password    string
	Skip        bool
	SkipMessage string
}

GraphicsEndpoint is the resolved, dialable VM graphics endpoint a vnc/spice verb gets from CheckContext.ResolveGraphicsEndpoint. Exactly one of Addr / Socket is set (the host bridges a UNIX socket to TCP for a TCP-only client, or forwards a remote listener, before returning). Password is the resolved ticket ("" = no auth). Skip=true (with SkipMessage) means the deployment declares no graphics device of that kind — an N/A skip, not a failure.

type HTTPRequest

type HTTPRequest struct {
	Method            string
	URL               string
	Body              []byte
	Headers           map[string]string
	Timeout           string
	AllowInsecure     bool
	NoFollowRedirects bool
	CAPEM             []byte
}

HTTPRequest is the host-vantage HTTP request a check verb hands cc.HTTPDo. It carries the FULL request plus the per-request policy the host needs to build the client: Timeout is a Go duration string ("" = the engine's base timeout); CAPEM is the resolved CA PEM bytes (a candy reads its authored ca_file host-side and ships the bytes, so the host server needs no filesystem access). Both placements (in-proc + the CheckContextService RPC) consume the SAME struct.

type HTTPResponse

type HTTPResponse struct {
	Status     int
	Body       []byte
	HeaderBlob string
}

HTTPResponse is the result of cc.HTTPDo: the status code, the response body, and the response headers as a pre-formatted "Key: value\n" blob (the host formats once — R3 — preserving multi-value headers the matcher pipeline consumes directly). A transport-level failure is returned as the HTTPDo error, not here.

func DoHTTPRequest added in v0.2026197.1320

func DoHTTPRequest(ctx context.Context, base *http.Client, req HTTPRequest) (HTTPResponse, error)

DoHTTPRequest issues req from the HOST's network namespace using a client built from base + req's per-request policy, returning the status, the body, and the formatted response-header blob. The ONE host-side HTTP-do path shared by the in-proc check context AND the CheckContextService reverse channel (R3). A transport-level failure is returned as err; a non-2xx is NOT an error (the caller matches resp.Status).

type ImportEntry added in v0.2026194.1400

type ImportEntry struct {
	Namespace string // "" = flat import into the current root namespace
	Ref       string // local path or `@host/org/repo[/sub/path]:version`
}

ImportEntry is one parsed `import:` list item. A flat entry (Namespace == "") merges the referenced file into the current root namespace; a namespaced entry mounts the referenced project under Namespace.

type ImportList added in v0.2026194.1400

type ImportList []ImportEntry

ImportList is the `import:` field type. Custom YAML decoding accepts a list whose items are either a bare string (flat) or a single-key mapping `alias: ref` (namespaced child import).

func (ImportList) MarshalYAML added in v0.2026194.1400

func (il ImportList) MarshalYAML() (any, error)

MarshalYAML emits each entry compactly: a flat entry as a scalar string, a namespaced entry as a single-key `alias: ref` map — the same shapes UnmarshalYAML accepts (round-trip safe; used by migrators that write configs).

func (*ImportList) UnmarshalYAML added in v0.2026194.1400

func (il *ImportList) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes the mixed-shape import list.

type InspectConfig added in v0.2026197.1320

type InspectConfig struct {
	Hostname string   `json:"Hostname"`
	Env      []string `json:"Env"`
}

InspectConfig carries the fields inside the "Config" object that we need: the hostname (container's internal name) and the effective env.

type InspectHostConfig added in v0.2026197.1320

type InspectHostConfig struct {
	NetworkMode string `json:"NetworkMode"`
}

InspectHostConfig carries the fields inside the "HostConfig" object that we need — currently just NetworkMode so host-networked containers can be detected (their NetworkSettings.Ports is empty, but container ports are bound 1:1 to host ports).

type InspectMount added in v0.2026197.1320

type InspectMount struct {
	Type        string `json:"Type"`
	Name        string `json:"Name,omitempty"`
	Source      string `json:"Source"`
	Destination string `json:"Destination"`
}

InspectMount describes a single mount attached to the container. For named volumes Name is set and Source is the volume's _data path; for bind mounts Name is empty and Source is the host directory.

type InspectNetwork added in v0.2026197.1320

type InspectNetwork struct {
	IPAddress string                        `json:"IPAddress"`
	Networks  map[string]InspectNetworkBind `json:"Networks"`
	// Ports keys are like "6379/tcp"; values are nil when unexposed or
	// a slice of bindings when published.
	Ports map[string][]InspectPortBind `json:"Ports"`
}

InspectNetwork carries IP address and port-binding data. Both top-level IPAddress and per-network IPAddress are captured — podman rootless with the default network uses the per-network form; docker bridge uses the top-level form.

type InspectNetworkBind added in v0.2026197.1320

type InspectNetworkBind struct {
	IPAddress string `json:"IPAddress"`
}

InspectNetworkBind is the per-network record under NetworkSettings.Networks.

type InspectPortBind added in v0.2026197.1320

type InspectPortBind struct {
	HostIp   string `json:"HostIp"`
	HostPort string `json:"HostPort"`
}

InspectPortBind is the host-side record of a port publication.

type JumpKind added in v0.2026190.848

type JumpKind int

JumpKind classifies how NestedExecutor enters the child environment. This determines both the shell-wrapper syntax for RunSystem/RunUser and the file-transport strategy for PutFile.

const (
	// JumpPodmanExec enters a rootful or rootless podman container
	// via `podman exec -i <name>`. The parent must have podman
	// available (the container-nesting candy provides this for
	// container-in-container; the virtualization candy is unrelated
	// here — container children of container parents).
	JumpPodmanExec JumpKind = iota + 1

	// JumpDockerExec enters a docker container via `docker exec -i`.
	// Separate from podman because docker and podman CLIs differ in
	// exit-code propagation and stdin handling.
	JumpDockerExec

	// JumpSSH makes an additional ssh hop — used when the child
	// itself is an SSH-reachable VM inside an already-SSH-reachable
	// parent (vm-in-container, vm-in-vm).
	JumpSSH

	// JumpVirshConsole attaches to a libvirt guest's serial console
	// for emergency-only shell access (used when the guest is
	// unreachable over SSH). Best-effort; primarily a diagnostic path.
	JumpVirshConsole
)

type LabelDescriptionSet added in v0.2026191.2205

type LabelDescriptionSet = spec.LabelDescriptionSet

func MergeDeployDescriptions added in v0.2026197.1537

func MergeDeployDescriptions(baked *LabelDescriptionSet, localPlan []spec.Step, originName string) *LabelDescriptionSet

MergeDeployDescriptions overlays a deployment node's local `plan:` steps onto a label-baked LabelDescriptionSet's Deploy section. A baked deploy step with the same step id is replaced by the local one; otherwise the local step is appended. This is the per-host override surface for acceptance steps (charly.yml deploy entries). If localPlan is empty, returns baked unchanged.

type LabeledDescription added in v0.2026191.2205

type LabeledDescription = spec.LabeledDescription

LabeledDescription + LabelDescriptionSet are CUE-sourced in spec (boxmetadata.cue, P2B, #60) and ALIASED here — spec.BoxMetadata.Description contains a *LabelDescriptionSet and spec sits below kit, so containment forces them down to spec (like VolumeMount → spec). Consumers keep using kit.LabelDescriptionSet / kit.LabeledDescription unchanged; the IsEmpty method lives in spec/labelset_methods.go beside the type.

type LedgerLock added in v0.2026190.848

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

LedgerLock is an acquired advisory lock on the ledger directory. Call Release() when done. Panic-safe via defer.

func AcquireLedgerLock added in v0.2026190.848

func AcquireLedgerLock(paths *LedgerPaths) (*LedgerLock, error)

AcquireLedgerLock takes a blocking exclusive flock on the ledger lock file via the shared AcquireFileLock primitive (filelock.go). Blocks until the lock is available.

func (*LedgerLock) Release added in v0.2026190.848

func (l *LedgerLock) Release() error

Release releases the flock and closes the file.

type LedgerPaths added in v0.2026190.848

type LedgerPaths struct {
	Root     string // ~/.config/opencharly/installed
	Deploys  string // <Root>/deploys/
	Candies  string // <Root>/layers/
	LockFile string // <Root>/.lock
}

LedgerPaths describes where ledger files live on disk. Extracted so tests can redirect to a temp dir.

func DefaultLedgerPaths added in v0.2026190.848

func DefaultLedgerPaths() (*LedgerPaths, error)

DefaultLedgerPaths returns the canonical paths anchored at the invoking user's home directory.

func (*LedgerPaths) Ensure added in v0.2026190.848

func (p *LedgerPaths) Ensure() error

Ensure creates the ledger directory tree if missing.

type LocalImageInfo added in v0.2026197.1320

type LocalImageInfo struct {
	ID     string            // image ID (sha256:...) — used by `charly clean` to skip in-use images
	Names  []string          // Full refs: ["ghcr.io/opencharly/jupyter:latest", ...]
	Labels map[string]string // OCI labels from the image config
	Size   int64             // reported storage size in bytes (podman's "Size" field; 0 if absent/unparsed)
}

LocalImageInfo describes an image present in the engine's local storage. Populated by ListLocalImages from `{podman,docker} images --format json`.

func ParseLocalImagesJSON added in v0.2026197.1320

func ParseLocalImagesJSON(out []byte) ([]LocalImageInfo, error)

ParseLocalImagesJSON parses `{podman,docker} images --format json` output into ONE LocalImageInfo per distinct image ID, with that id's tag refs merged into Names.

This dedup is load-bearing: podman emits ONE ROW PER TAG, and each row's Names array already lists EVERY tag on that id. A naive row-by-row mapping therefore produces N near-identical entries for an id with N tags — which made `charly clean`'s keep-N retention over-count entries and, worse, remove an id's whole Names array per "extra" entry (deleting tags it meant to keep). Collapsing to one-id-with-a-tag-list matches the struct's shape and what every consumer (retention prune, short-name resolver) expects.

Empty-id rows (dangling/untagged) are kept separate via a per-row sentinel key so they never merge into one another.

type MethodSpec

type MethodSpec struct {
	// Path is the `charly check <verb> <method...>` subcommand path.
	Path []string
	// Required names the plugin-input fields that must be set for this method.
	Required []string
	// PosArgs builds the positional args inserted after the image name, before -i.
	PosArgs func(c *spec.Op) []string
	// Artifact marks a state-dependent capture method (screenshot) whose produced
	// file is validated.
	Artifact bool
	// SkipBox = true means the verb targets a cluster/other non-image target, so the
	// usual image/deploy-name positional is NOT inserted.
	SkipBox bool
}

MethodSpec is one method's nested-CLI dispatch spec: the `charly check <verb> <method...>` subcommand path, the required #Op modifiers, the positional-arg builder, and the artifact / skip-box flags. A plugin's method allowlist is a map[string]MethodSpec; fields are exported so a candy module can author it.

type NestedExecutor added in v0.2026190.848

type NestedExecutor struct {
	Parent spec.DeployExecutor
	Jump   NestedJump
}

NestedExecutor wraps a parent DeployExecutor and rewrites each DeployExecutor primitive to run inside a nested environment via Jump. Stacks: a NestedExecutor can itself be the Parent of another NestedExecutor, yielding arbitrary-depth composition.

func (*NestedExecutor) GetFile added in v0.2026190.848

func (n *NestedExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)

GetFile retrieves a file from the nested environment. Current implementation is a minimal read-then-retrieve: the parent shell runs `<jump> cat <path>` and the output is captured via a staged tmp file on the parent venue, then Parent.GetFile pulls it back. Jumps without an obvious cat+stdout path (libvirt-console) return an explicit "not supported" error rather than silently producing empty bytes.

func (*NestedExecutor) Kind added in v0.2026190.848

func (n *NestedExecutor) Kind() string

Kind reports the venue's coarse classification, derived from the LEAF jump (this NestedExecutor's own Jump). The parent chain doesn't affect Kind because tests care about what their probe lands in, not the path taken to reach it.

func (*NestedExecutor) PutFile added in v0.2026190.848

func (n *NestedExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts EmitOpts) error

PutFile transfers a file from the invoking host into the nested venue. Implementation picks a FileTransport based on Jump.Kind: podman/docker containers use `<engine> cp`, SSH hops use scp, libvirt console uses virt-copy-in (best effort).

For stacked jumps, the transfer is multi-stage: first land the file at a tmp path on the parent venue (via Parent.PutFile), then issue a one-shot move-into-child command via RunSystem. This preserves the "every executor only knows its own hop" invariant and keeps the FileTransport implementation simple.

func (*NestedExecutor) ResolveHome added in v0.2026190.848

func (n *NestedExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` inside the leaf environment of this nested executor. Implementation: shell out a small `getent passwd` command via RunCapture (which already routes through the jump chain). Empty user resolves to the leaf-shell's $HOME.

func (*NestedExecutor) RunBuilder added in v0.2026190.848

func (n *NestedExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)

RunBuilder always runs on the outermost host (where podman with image-building capabilities lives). Delegates to the parent chain unchanged — nested executors never run builders inside themselves. Artifacts produced by the builder land on the outer host and are distributed to the nested environment via PutFile.

func (*NestedExecutor) RunCapture added in v0.2026190.848

func (n *NestedExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a script inside the nested venue and returns stdout/stderr/exit. The wrapped script is forwarded to the parent's own RunCapture, so multi-hop chains compose: harness sandbox (host) → VM (ssh) → inner-pod (podman exec) → nested-pod (podman exec) stacks four heredocs and captures the deepest stdout.

asRoot is hard-coded to false here: test verbs probe state and add `sudo` explicitly when needed (matching the pre-cutover Executor.Exec semantics). Adding root escalation would silently change probe behaviour for every existing test.

func (*NestedExecutor) RunInteractive added in v0.2026195.501

func (n *NestedExecutor) RunInteractive(ctx context.Context, script string) (int, error)

func (*NestedExecutor) RunStream added in v0.2026195.501

func (n *NestedExecutor) RunStream(ctx context.Context, script string) (int, error)

func (*NestedExecutor) RunSystem added in v0.2026190.848

func (n *NestedExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error

RunSystem routes a bash script through the jump as root.

func (*NestedExecutor) RunUser added in v0.2026190.848

func (n *NestedExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error

RunUser runs as the unprivileged user inside the nested environment.

func (*NestedExecutor) StartProcess added in v0.2026200.1312

func (n *NestedExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess composes the leaf argv into one argv for the parent process executor. This makes arbitrary container/SSH nesting recursive and keeps the gRPC transport unaware of deployment kinds.

func (*NestedExecutor) Venue added in v0.2026190.848

func (n *NestedExecutor) Venue() string

Venue returns "nested:<Jump>/<parentVenue>". The parent venue is appended so the full chain is preserved — useful for ledger keying and debug output (e.g. "nested:podman-exec:mychild/ssh://arch@…").

type NestedJump added in v0.2026190.848

type NestedJump struct {
	Kind   JumpKind
	Target string

	// Extra arguments inserted before the shell invocation. Rarely
	// needed; useful for `podman exec --env FOO=bar` or
	// `ssh -o ProxyJump=…` style tweaks.
	ExtraArgs []string
}

NestedJump describes one hop into a nested environment. The Target string's meaning depends on Kind:

JumpPodmanExec / JumpDockerExec: container name.
JumpSSH:                         "[user@]host[:port]" or an
                                 ssh-config alias (e.g.
                                 "charly-<vmname>"). ssh(1) reads
                                 ~/.ssh/config + agent for keys
                                 and connection options — we
                                 contain zero credential state.
JumpVirshConsole:                libvirt domain name.

func (NestedJump) String added in v0.2026190.848

func (j NestedJump) String() string

String renders a jump as a human-readable venue segment. Used as a component of NestedExecutor.Venue().

type ParsedPortMapping added in v0.2026190.848

type ParsedPortMapping struct {
	BindAddr  string // explicit bind prefix if present (e.g. "127.0.0.1" or "[::1]"); empty otherwise
	Host      int
	Container int
	Protocol  string // "udp" / "tcp" / "" — extracted from /udp or /tcp suffix
}

ParsedPortMapping describes the four possible shapes podman accepts:

"P"               -> {Host: P, Container: P}
"H:C"             -> {Host: H, Container: C}
"IP:H:C"          -> {Host: H, Container: C, BindAddr: "IP"}
"[v6]:H:C"        -> {Host: H, Container: C, BindAddr: "[v6]"}

Any of those forms may carry a /tcp or /udp suffix on the trailing port.

func AllocateAutoPorts added in v0.2026190.848

func AllocateAutoPorts(containerPorts []int, occupied map[int]bool) ([]ParsedPortMapping, error)

AllocateAutoPorts probes free TCP host ports — one per container port, in declaration order. The `occupied` set names host ports already in use by other deployments (so two `port: [auto]` deploys on the same host don't collide). Returned mappings have BindAddr="" (default host bind) and Protocol="tcp". Each successful allocation is recorded back into `occupied` so subsequent calls in the same BundleConfig pass see the reservation.

Free-port discovery uses the same net.Listen("tcp","127.0.0.1:0") + immediate-close pattern already used by ssh_tunnel.go:78 and vnc_helpers.go's unixToTcpBridge — the OS picks an ephemeral port, we close, and the caller binds in the (small) window before the OS reassigns.

func ParsePortMapping added in v0.2026190.848

func ParsePortMapping(mapping string) (ParsedPortMapping, bool)

ParsePortMapping is the canonical port-mapping parser.

Returns ok=false on unparseable input. Callers that want a loud failure (warning logged, port skipped) should branch on ok.

All in-tree port handling routes through this — ParseHostPort, ParseContainerPort, parseHostPorts (tunnel.go), buildPortMapping (tunnel.go), and localizePort (shell.go) — so a single fix here covers every site that would otherwise mis-handle the IP:H:C form.

type ParsedRef added in v0.2026198.345

type ParsedRef struct {
	Raw      string // original string, e.g. "@github.com/org/repo/candy/name:v1.0.0"
	RepoPath string // e.g. "github.com/org/repo"
	SubPath  string // e.g. "candy/name" (path within repo)
	Name     string // e.g. "name" (last segment)
	Version  string // e.g. "v1.0.0"
}

ParsedRef represents a parsed remote reference with version. Works for both candy refs and image refs. Format: @host/org/repo/sub/path:version

func ParseRemoteRef added in v0.2026198.345

func ParseRemoteRef(ref string) *ParsedRef

ParseRemoteRef parses a remote reference into repo path, sub-path, name, and version. e.g. "@github.com/org/repo/candy/name:v1.0.0" -> ParsedRef{RepoPath: "github.com/org/repo", SubPath: "candy/name", Name: "name", Version: "v1.0.0"}

type PlanContext added in v0.2026191.2205

type PlanContext interface {
	// Distros is the image's distro tag list, for the exclude_distros: skip.
	Distros() []string
	// Mode selects RunModeLive vs RunModeBox routing.
	Mode() RunMode
	// VerifyOnly restricts a plan walk to idempotent verify steps (check:/agent-check:),
	// skipping mutating steps (run:/agent-run:) — the check live / check box mode.
	VerifyOnly() bool
	// SkipDeterministicRun skips deterministic run: install-timeline steps while still
	// running check:/agent-check: and the agent-graded agent-run: — the feature-run mode.
	SkipDeterministicRun() bool
	// ContextSkipReason returns a non-empty skip message when the op's effective execution
	// context is not active in the current mode (box→build, live→runtime); "" means the op
	// runs. Wraps the core VerbCatalog grammar so ExecContext never crosses this seam.
	ContextSkipReason(op *spec.Op) string
	// EffectiveDo resolves the op's do-mode (the keyword-stamped intentDo, else the verb's
	// VerbCatalog default, else DoAssert). Wraps the core grammar.
	EffectiveDo(op *spec.Op) spec.DoMode
	// EffectiveEnv builds the variable-expansion env for the current step (the resolver base
	// overlaid with cross-deployment ${HOST:…} addresses + the scenario captures).
	EffectiveEnv() map[string]string
	// ProbeNeverHang is the per-probe-attempt never-hang ceiling for op.
	ProbeNeverHang(op *spec.Op) time.Duration
	// SwapVenue retargets the host executor/resolver to op's per-step venue for the duration
	// of one dispatch, returning a restore func (nil when no swap) and a non-empty failReason
	// when the venue could not be resolved (the op is reported FAIL). The core impl mutates
	// the *Runner it wraps, so EffectiveEnv + the verb dispatch see the swapped venue.
	SwapVenue(op *spec.Op) (restore func(), failReason string)
	// Scenario is the per-run capture/var context; SetScenario installs a fresh one for a
	// RunPlan walk (restored by the caller).
	Scenario() *ScenarioContext
	SetScenario(sc *ScenarioContext)
	// Verbs is the verb-dispatch seam; Grader is the agent-step grader (nil when unbound).
	Verbs() VerbResolver
	Grader() StepGrader
}

PlanContext is the host-driver surface the plan walk consumes. The core *Runner implements it; a plugin running a plan in-proc supplies its own impl. Every method is kind-blind — no concrete kind, no provider word crosses this interface (spec.ExecContext is a plain vocabulary type; the grammar that consults it stays core behind ContextSkipReason / EffectiveDo).

type PlanGrammar added in v0.2026194.1605

type PlanGrammar interface {
	// EffectiveDo resolves op's do-mode (the keyword-stamped intentDo wins, else the verb's
	// VerbCatalog default, else DoAssert).
	EffectiveDo(op *spec.Op) spec.DoMode
	// InContext reports whether op is legal in the run's active context: runtime=true → the
	// live (runtime) context, runtime=false → the box (build) context.
	InContext(op *spec.Op, runtime bool) bool
	// ContextsLabel is op's effective-contexts list pre-formatted for the context-skip message.
	ContextsLabel(op *spec.Op) string
}

PlanGrammar is the do-mode + execution-context grammar seam. charly core's VerbCatalog impl stays core (ExecContext lives in deploykit, which imports kit, so the concrete context enum never crosses this seam: the context predicate is a bool and the skip-message contexts a pre-formatted string).

type PollCond added in v0.2026186.739

type PollCond func(ctx context.Context) (ready bool, progress float64, err error)

PollCond is a per-tick readiness probe (ready?, progress, err) — the same shape the host poll primitive expects.

type PollCondition added in v0.2026190.848

type PollCondition = vmshared.PollCondition

type PollFunc added in v0.2026186.739

type PollFunc func(ctx context.Context, cond PollCond) error

PollFunc drives a PollCond to readiness under the caller's readiness-configured bounds. Core and the vm plugin inject one wrapping vmshared's pollUntil + the resolved remote bounds, so kit never imports the readiness/poll subsystem.

type PortConflict added in v0.2026190.848

type PortConflict struct {
	HostPort  int
	ContPort  int
	Owner     string // container name, or "unknown"
	OwnerType string // "charly-container", "container", "host-process"
}

PortConflict describes a host port that is already in use.

func CheckPortAvailability added in v0.2026190.848

func CheckPortAvailability(ports []string, bindAddr string, engine string) []PortConflict

CheckPortAvailability tests whether each host port can be bound. Returns a list of conflicts for ports that are already in use. Detects /udp suffix and uses UDP bind check accordingly.

type ProvisionActor

type ProvisionActor interface {
	Reserved() string
	RenderProvisionScript(op *spec.Op, distros []string) (script string, ok bool)
}

ProvisionActor is the OPTIONAL second role of a host-coupled verb candy: the do:act renderer for a state-provision verb (kernel_param/mount/user/unix_group/file/command/ service/package), rendering the shell that ENACTS the op under the live init / package manager. It is reached at install COMPILE+EMIT (a `run: {plugin: <verb>}` step → the build-act RUN in emitTasks, and the local/vm deploy act) AND at runtime act. A candy whose verb type implements this ALONGSIDE CheckVerbProvider is registered as a multi-role provider (the host adapter then also satisfies the package-main ProvisionActor). op is the spec.Op (the verb's plugin_input rides op.PluginInput); distros is the image's distro tag list for package-name resolution. Returns (script, ok); ok=false means "no act form for this op" (the host skips/errors per its act path). This is the SHELL-string act role — a verb that instead lowers into a typed InstallPlan step (service/package) additionally needs the kit step contract.

type RefsDownloader added in v0.2026192.1026

type RefsDownloader = spec.RefsDownloader

refs_downloader.go — the swappable remote-repo FETCH BACKEND seam (P7). The interface itself relocated to sdk/spec (FLOOR-SLIM axis-A mechanical batch, alongside DocParser/ProjectWalker/ CandyScanner) so charly core's plugin_inproc.go can type-assert against it without importing kit; aliased here so every existing kit.RefsDownloader reference (candy/plugin-refs, charly/refs_threaded.go) keeps compiling unchanged. Only the DOWNLOAD is pluggable; the host keeps the fetch ORCHESTRATION (local-override resolution, cache-hit short-circuit, and the post-fetch schema auto-migration) — the boundary is the backend that turns a (repoPath, version) into a populated local cache tree.

type ResolvedReadiness added in v0.2026190.848

type ResolvedReadiness = vmshared.ResolvedReadiness

type ResolvedRuntime added in v0.2026190.848

type ResolvedRuntime struct {
	BuildEngine          string // "docker" or "podman"
	RunEngine            string // "docker" or "podman"
	Rootful              string // "auto", "machine", "sudo", "native"
	RunMode              string // "direct" or "quadlet"
	AutoEnable           bool   // auto-enable quadlet on first start
	BindAddress          string // "127.0.0.1" or "0.0.0.0"
	EncryptedStoragePath string // path for gocryptfs encrypted storage
	VolumesPath          string // base path for bind mount volume data
	ForwardGpgAgent      bool   // forward host GPG agent socket into containers
	ForwardSshAgent      bool   // forward host SSH agent socket into containers
	VmBackend            string // "auto", "libvirt", or "qemu"
}

ResolvedRuntime holds the fully resolved runtime configuration

func ResolveRuntime added in v0.2026190.848

func ResolveRuntime() (*ResolvedRuntime, error)

ResolveRuntime resolves the runtime configuration: env vars > config file > defaults.

type Result

type Result struct {
	Status        Status
	Message       string
	CapturedValue string // value stashed under `capture:` (recorded only on PASS)
}

Result is a host-coupled verb's verdict. charly converts it to its internal CheckResult (stamping the Op/Verb/timing) at the dispatch boundary.

func Fail

func Fail(msg string) Result

func Failf

func Failf(format string, a ...any) Result

func Pass

func Pass(msg string) Result

Pass / Fail / Skip are the verdict constructors a verb returns; the *f variants take a printf format (mirror charly's passf/failf/skipf).

func Passf

func Passf(format string, a ...any) Result

func Skip

func Skip(msg string) Result

func Skipf

func Skipf(format string, a ...any) Result

type ReverseExecutor added in v0.2026190.848

type ReverseExecutor interface {
	ReverseDryRun() bool
	ReverseKeepRepoChanges() bool
	ReverseKeepServices() bool
	ReverseRunner() ReverseRunner
}

ReverseExecutor is the interface ReverseOp handlers expect. Allows us to pass either BundleDelCmd (for real teardown) or a test mock.

ReverseRunner returns the shell-runner used to execute reversal commands. When non-nil, handlers dispatch through it (so VM teardown runs commands over SSH instead of locally); when nil, handlers fall back to local `exec.Command` — the long-standing host-teardown path.

type ReverseOp added in v0.2026190.848

type ReverseOp = spec.ReverseOp

type ReverseOpKind added in v0.2026190.848

type ReverseOpKind = spec.ReverseOpKind

type ReverseRunner added in v0.2026190.848

type ReverseRunner interface {
	// RunSystem runs a bash script as root (wraps with sudo on host
	// runners; uses `ssh sudo bash -s` on VM runners).
	RunSystem(script string) error
	// RunUser runs a bash script as the deploy user (no sudo).
	RunUser(script string) error
}

ReverseRunner executes reversal shell scripts at the requested privilege level. Implemented by both the local-exec host runner and the SSH-based VM runner (SSHReverseRunner, below).

type RunMode

type RunMode int

RunMode mirrors charly's RunMode: the mode a check runs under.

const (
	// ModeLive — `charly check live`, against a running container/VM (in-container probes).
	ModeLive RunMode = iota
	// ModeBox — `charly check box`, against a disposable build container.
	ModeBox
)

func (RunMode) String

func (m RunMode) String() string

String renders the mode as "box" / "live" (mirrors charly's runModeName).

type Runner added in v0.2026194.1605

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

Runner wires the execution context for one pass of checks. Constructor-shaped: build it with NewRunner and reach its state through the methods below.

func NewRunner added in v0.2026194.1605

func NewRunner(cfg RunnerConfig) *Runner

NewRunner constructs a Runner from cfg, applying defaults for a zero HTTPClient (10s), DialTimeout (3s), and ProbeTimeout (the fallback ceiling; the host normally supplies its readiness-derived value).

func (*Runner) Box added in v0.2026194.1605

func (r *Runner) Box() string

Box / Instance are the deployment's image + instance names (empty under ModeBox).

func (*Runner) CandyDirs added in v0.2026194.1605

func (r *Runner) CandyDirs() map[string]string

func (*Runner) CandyScanErr added in v0.2026194.1605

func (r *Runner) CandyScanErr() error

func (*Runner) ContextSkipReason added in v0.2026194.1605

func (r *Runner) ContextSkipReason(op *spec.Op) string

ContextSkipReason returns a non-empty skip message when op's effective execution context is not active in the run's mode (box→build, live→runtime); "" means the op runs. The mode-composition and the message formatting are kit-side; only the VerbCatalog context predicate crosses the grammar seam (a nil grammar — a bare-Op test — never skips on context).

func (*Runner) DialTimeout added in v0.2026194.1605

func (r *Runner) DialTimeout() time.Duration

DialTimeout is the per-dial ceiling for host-side TCP reachability probes. HTTPClient is the engine's base client (the host HTTPDo leg derives per-request clients from it). HasRuntime reports whether the resolved env carries running-container state (false → runtime-only vars resolve to a skip). CandyDirs / CandyScanErr anchor a relative committed-APK path.

func (*Runner) Distros added in v0.2026194.1605

func (r *Runner) Distros() []string

Distros is the image's distro tag list, for the exclude_distros: skip.

func (*Runner) EffectiveDo added in v0.2026194.1605

func (r *Runner) EffectiveDo(op *spec.Op) spec.DoMode

EffectiveDo resolves op's do-mode via the injected grammar (DoAssert when no grammar is wired).

func (*Runner) EffectiveEnv added in v0.2026194.1605

func (r *Runner) EffectiveEnv() map[string]string

EffectiveEnv builds the variable-expansion env for the current step: the resolver base overlaid with cross-deployment ${HOST:…} addresses (per-run, target-independent) then the scenario captures (which win on a key collision). Copy-on-overlay keeps the base map clean across runs.

func (*Runner) Exec added in v0.2026194.1605

func (r *Runner) Exec() Executor

Exec is the venue executor for the current (possibly venue-swapped) target.

func (*Runner) Grader added in v0.2026194.1605

func (r *Runner) Grader() StepGrader

func (*Runner) HTTPClient added in v0.2026194.1605

func (r *Runner) HTTPClient() *http.Client

func (*Runner) HasRuntime added in v0.2026194.1605

func (r *Runner) HasRuntime() bool

func (*Runner) Instance added in v0.2026194.1605

func (r *Runner) Instance() string

func (*Runner) Mode added in v0.2026194.1605

func (r *Runner) Mode() RunMode

Mode selects ModeLive vs ModeBox routing.

func (*Runner) ProbeNeverHang added in v0.2026194.1605

func (r *Runner) ProbeNeverHang(op *spec.Op) time.Duration

ProbeNeverHang is the per-probe-attempt never-hang ceiling for op. It is NOT the probe's semantic timeout (the http client, dial timeout, a verb's own timeout:, and the eventually: retry all operate INSIDE it) — it is the kill-switch for a probe that wedges in its data phase. A longer author-declared timeout: is honored over the floor so a slow probe is never cut short.

func (*Runner) Run added in v0.2026194.1605

func (r *Runner) Run(ctx context.Context, checks []spec.Op) []CheckResult

Run executes the supplied checks sequentially and returns per-check results. It does not short-circuit on failure — the report shows every check's outcome. The per-check walk lives in RunOne; Runner is its driver (it implements PlanContext).

func (*Runner) Scenario added in v0.2026194.1605

func (r *Runner) Scenario() *ScenarioContext

Scenario is the per-run capture/var context; SetScenario installs one for a RunPlan walk.

func (*Runner) SetScenario added in v0.2026194.1605

func (r *Runner) SetScenario(sc *ScenarioContext)

func (*Runner) SkipDeterministicRun added in v0.2026194.1605

func (r *Runner) SkipDeterministicRun() bool

SkipDeterministicRun skips deterministic run: install-timeline steps (feature-run mode).

func (*Runner) SwapVenue added in v0.2026194.1605

func (r *Runner) SwapVenue(op *spec.Op) (func(), string)

SwapVenue retargets the executor + env + image to op's per-step venue for the duration of one dispatch, returning a restore func (nil when no swap) and a non-empty failReason when the venue cannot be resolved. It mutates the Runner in place so EffectiveEnv + the verb dispatch (which read exec/env/box) see the swapped venue — the same self-swap guard the classical inline path used (venue set, differs from the active target, and a TargetResolver is wired).

func (*Runner) Verbs added in v0.2026194.1605

func (r *Runner) Verbs() VerbResolver

Verbs is the verb-dispatch seam; Grader is the agent-step grader (nil when unbound).

func (*Runner) VerifyOnly added in v0.2026194.1605

func (r *Runner) VerifyOnly() bool

VerifyOnly restricts the walk to idempotent verify steps (check:/agent-check:).

func (*Runner) VmName added in v0.2026194.1605

func (r *Runner) VmName() string

VmName is the caller-set VM domain-target (the resolved per-deploy domain identity; empty for non-VM deployments); VmTargetName falls back to Box, the name the host vm/spice legs hand the plugin as the libvirt-domain target.

func (*Runner) VmTargetName added in v0.2026194.1605

func (r *Runner) VmTargetName() string

type RunnerConfig added in v0.2026194.1605

type RunnerConfig struct {
	Exec Executor
	Mode RunMode

	// Env is the resolved variable map (the host CheckVarResolver.Env); HasRuntime mirrors
	// CheckVarResolver.HasRuntime (false → runtime-only vars resolve to an unresolved skip).
	Env        map[string]string
	HasRuntime bool

	Distros  []string
	Box      string
	Instance string
	// VmName is the VM domain-target name the host vm/spice/libvirt verb legs address for a VM
	// deployment: the caller sets it to the already-resolved per-deploy domain identity
	// (charly-<VmName> is the live libvirt domain). Empty for non-VM deployments, where
	// VmTargetName falls back to Box.
	VmName string

	HostVars map[string]string
	// CandyDirs maps candy name → resolved source dir (relative committed-APK anchoring);
	// CandyScanErr is its build error (only an apk-anchoring check consults it). Host-read.
	CandyDirs    map[string]string
	CandyScanErr error

	VerifyOnly           bool
	SkipDeterministicRun bool

	Scenario *ScenarioContext
	Grader   StepGrader

	// Injected host seams (all required for a live run; a bare-Op test may leave Grammar/
	// TargetResolver nil — SwapVenue/ContextSkipReason then no-op, matching the classical path).
	Verbs          VerbResolver
	Grammar        PlanGrammar
	TargetResolver VenueResolver

	// Optional overrides — zero values take the NewRunner defaults.
	DialTimeout  time.Duration
	ProbeTimeout time.Duration
	HTTPClient   *http.Client
}

RunnerConfig carries every field a Runner is built with. The host fills it (folding the former post-construction field-pokes + attachCheckRunnerContext); zero DialTimeout/ProbeTimeout/ HTTPClient fall back to sensible defaults in NewRunner.

type RuntimeConfig added in v0.2026190.848

type RuntimeConfig struct {
	Engine                 EngineConfig      `yaml:"engine" json:"engine"`
	RunMode                string            `yaml:"run_mode,omitempty" json:"run_mode,omitempty"`
	AutoEnable             *bool             `yaml:"auto_enable,omitempty" json:"auto_enable,omitempty"`
	BindAddress            string            `yaml:"bind_address,omitempty" json:"bind_address,omitempty"`
	EncryptedStoragePath   string            `yaml:"encrypted_storage_path,omitempty" json:"encrypted_storage_path,omitempty"`
	VolumesPath            string            `yaml:"volumes_path,omitempty" json:"volumes_path,omitempty"`
	SecretBackend          string            `yaml:"secret_backend,omitempty" json:"secret_backend,omitempty"`       // "auto", "keyring", "config"
	ForwardGpgAgent        *bool             `yaml:"forward_gpg_agent,omitempty" json:"forward_gpg_agent,omitempty"` // Forward host GPG agent socket into containers (default: true)
	ForwardSshAgent        *bool             `yaml:"forward_ssh_agent,omitempty" json:"forward_ssh_agent,omitempty"` // Forward host SSH agent socket into containers (default: true)
	Vm                     RuntimeVmConfig   `yaml:"vm,omitempty" json:"vm,omitempty"`
	VncPasswords           map[string]string `yaml:"vnc_passwords,omitempty" json:"vnc_passwords,omitempty"`                       // VNC passwords keyed by image[-instance]
	KeyringKeys            []string          `yaml:"keyring_keys,omitempty" json:"keyring_keys,omitempty"`                         // Shadow index: names of keys stored in keyring (no values)
	KeyringCollectionLabel string            `yaml:"keyring_collection_label,omitempty" json:"keyring_collection_label,omitempty"` // Preferred Secret Service collection label; empty means use default alias then iterate.
	// HostAliases maps short names (e.g. "o") to SSH targets (e.g.
	// "user@o.example.org"). Consulted by charly's --host flag when
	// re-execing commands on remote machines. Set via
	// `charly settings set hosts.<alias> <ssh-target>`.
	HostAliases map[string]string `yaml:"host_aliases,omitempty" json:"host_aliases,omitempty"`
}

RuntimeConfig represents the user-level runtime configuration (~/.config/charly/config.yml)

func LoadRuntimeConfig added in v0.2026190.848

func LoadRuntimeConfig() (*RuntimeConfig, error)

LoadRuntimeConfig reads the runtime config file. Returns zero-value config if missing.

type RuntimeVmConfig added in v0.2026190.848

type RuntimeVmConfig struct {
	Backend   string `yaml:"backend,omitempty" json:"backend,omitempty"`     // "auto", "libvirt", "qemu"
	DiskSize  string `yaml:"disk_size,omitempty" json:"disk_size,omitempty"` // default disk size
	RootSize  string `yaml:"root_size,omitempty" json:"root_size,omitempty"` // root partition size
	Ram       string `yaml:"ram,omitempty" json:"ram,omitempty"`             // default RAM
	Cpus      int    `yaml:"cpus,omitempty" json:"cpus,omitempty"`           // default CPU count
	Rootfs    string `yaml:"rootfs,omitempty" json:"rootfs,omitempty"`       // root filesystem type
	Transport string `yaml:"transport,omitempty" json:"transport,omitempty"` // image transport (registry, containers-storage)
}

RuntimeVmConfig holds user-level VM defaults

type SSHArgs added in v0.2026186.739

type SSHArgs struct {
	User           string
	Host           string
	Port           int
	Args           []string
	ConnectTimeout int
}

SSHArgs is the host-surface ssh/scp coordinates for a managed VM alias.

func (SSHArgs) BaseArgs added in v0.2026186.739

func (a SSHArgs) BaseArgs() []string

BaseArgs builds the common ssh invocation prefix: only the per-call ergonomics (LogLevel, ConnectTimeout) + optional Port + pass-through Args + the "user@host"-or-"host" destination (ssh(1) reads ~/.ssh/config + ssh-agent for keys/host-key checking/identity).

func (SSHArgs) ScpBaseArgs added in v0.2026186.739

func (a SSHArgs) ScpBaseArgs() []string

ScpBaseArgs builds the scp invocation prefix (scp's `-P` is uppercase vs ssh's lowercase `-p`).

type SSHExecutor added in v0.2026190.848

type SSHExecutor struct {
	// User is the SSH login user. Optional — when empty, ssh(1) reads
	// the User directive from ~/.ssh/config or falls back to $USER.
	User string

	// Host is the SSH target — a hostname, an "[user@]host[:port]"
	// destination, or an ssh-config alias (e.g., the "charly-<vmname>"
	// stanzas managed by charly vm create). Required.
	Host string

	// Port is the SSH port. Optional — when 0, ssh uses the Port
	// directive from ~/.ssh/config or default 22.
	Port int

	// Args are extra ssh-cli arguments appended verbatim before the
	// destination. Pass-through from the deployment's `ssh_args:` field
	// (Ansible's ansible_ssh_extra_args). We do NOT parse, validate,
	// or interpret. Use sparingly — ssh-config is the right home for
	// persistent options.
	Args []string

	// ConnectTimeout caps the `-o ConnectTimeout=<N>` used in every
	// ssh invocation. Defaults to 10 seconds when zero.
	ConnectTimeout int
}

SSHExecutor implements DeployExecutor against an SSH-reachable guest. Used by the external vm deploy to run the same InstallPlan IR that the local deploy target runs — but wrapped as `ssh <user>@<host> sudo bash -s` instead of direct local bash, and scp for file transfers.

The builder-container path (VenueContainerBuilder steps for pixi/npm/cargo/aur) runs on the **host** (where podman is available), and the resulting artifacts are scp'd into the guest via PutFile. This keeps podman out of the guest's dependency surface.

Credential-free by design: SSHExecutor contains zero key paths, zero host-key overrides, zero ssh-agent socket detection. ssh(1) reads ~/.ssh/config and ssh-agent for everything. VMs publish a managed Host stanza into ~/.config/charly/ssh_config (Included from ~/.ssh/config) that names the IdentityFile + UserKnownHostsFile + StrictHostKeyChecking per VM; charly vm create writes the stanza, charly vm destroy removes it.

func (*SSHExecutor) GetFile added in v0.2026190.848

func (e *SSHExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)

GetFile retrieves the contents of a file from the guest via `ssh <host> [sudo] cat <path>` with stdout captured. asRoot==true wraps the read in sudo so restricted files (e.g. kubeconfig under /etc/rancher) are accessible from the unprivileged SSH user.

func (*SSHExecutor) Kind added in v0.2026190.848

func (e *SSHExecutor) Kind() string

Kind reports "ssh" — SSHExecutor targets any host reachable over SSH (VMs via the managed charly-<name> aliases, remote machines via "[user@]host[:port]" or ssh-config aliases).

func (*SSHExecutor) PutFile added in v0.2026190.848

func (e *SSHExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts EmitOpts) error

PutFile copies a local file into the guest via scp, then `install`s it into place with the correct mode/owner. This two-step dance is needed because scp runs as the guest's unprivileged user (you can't scp directly to /usr/local/bin).

ownerRoot decides the PRIVILEGE the install runs at — the SAME contract as ShellExecutor.PutFile (R3): ownerRoot=true → `sudo install -o root -g root` (a system-scoped file, root-owned); ownerRoot=false → a plain user-scoped `install` run AS THE GUEST USER (RunUser, NO sudo) so the file AND any parent dirs `install -D` creates are USER-owned. Running the non-root branch under sudo (the old bug — it always RunSystem'd) created root-owned `~/.config/opencharly/{env.d,…}` in the guest, which then blocked the user-scoped ledger write (`mkdir … Permission denied`). The prior in-proc VM target wrote env.d via RunUser for exactly this reason; the externalized walk reaches the same RunUser path through here.

func (*SSHExecutor) ResolveHome added in v0.2026190.848

func (e *SSHExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` on the SSH-reachable target. Empty user resolves to whoever ssh logged in as (via `echo $HOME`), non-empty user goes through `getent passwd <user>` so callers can resolve any user's home on the guest.

This replaces the bug where `the local deploy target.HostHome` was initialized from `os.Getenv("HOME")` — the operator's home, not the guest user's. Any subsequent shell-rc edit (env.d sourcing block, new shell:-schema managed-block, etc.) now lands in the right place.

func (*SSHExecutor) RunBuilder added in v0.2026190.848

func (e *SSHExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)

RunBuilder delegates to BuilderRun which runs the podman container *on the host*. Caller (the external vm deploy) is responsible for scp-ing the resulting artifacts into the guest via PutFile afterwards — this executor doesn't shuttle artifact trees itself.

func (*SSHExecutor) RunCapture added in v0.2026190.848

func (e *SSHExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a script on the guest and returns captured stdout/stderr/exit. Mirrors the deleted VmTestExecutor.Exec semantics: no automatic root escalation (callers that need root prefix sudo).

func (*SSHExecutor) RunInteractive added in v0.2026195.501

func (e *SSHExecutor) RunInteractive(_ context.Context, script string) (int, error)

func (*SSHExecutor) RunStream added in v0.2026195.501

func (e *SSHExecutor) RunStream(_ context.Context, script string) (int, error)

func (*SSHExecutor) RunSystem added in v0.2026190.848

func (e *SSHExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error

RunSystem executes a bash script as root on the guest. Wraps as `ssh vm 'sudo bash -s'` with the script fed on stdin.

func (*SSHExecutor) RunUser added in v0.2026190.848

func (e *SSHExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error

RunUser executes a bash script as the guest's unprivileged user (i.e. spec.SSH.User, the account SSHExecutor connects as).

func (*SSHExecutor) SSHBaseArgs added in v0.2026190.848

func (e *SSHExecutor) SSHBaseArgs() []string

SSHBaseArgs builds the common ssh invocation prefix. ssh(1) reads ~/.ssh/config + ssh-agent for keys, host-key checking, identity files, etc. We supply only the per-call ergonomics (LogLevel, ConnectTimeout) plus optional Port (when caller pre-parsed it from the destination string) plus the deployment's pass-through Args. The destination is "user@host" when User is set, otherwise just Host — letting ssh-config's User directive apply.

func (*SSHExecutor) StartProcess added in v0.2026200.1312

func (e *SSHExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess launches one exact argv through SSH. OpenSSH necessarily sends a remote command string; POSIX single quoting each token preserves the argv boundary and prevents interpolation by the remote login shell.

func (*SSHExecutor) Venue added in v0.2026190.848

func (e *SSHExecutor) Venue() string

Venue returns a stable "ssh://<user>@<host>:<port>" identifier so install_ledger.go can scope per-target ledgers without colliding with the local-shell ledger or other SSH targets. Components that are empty stringify naturally ("ssh://server" when User+Port unset).

func (*SSHExecutor) WaitForCloudInit added in v0.2026190.848

func (e *SSHExecutor) WaitForCloudInit(ctx context.Context) error

WaitForCloudInit polls `sudo cloud-init status` on the guest until cloud-init settles (status done/error/disabled). Only meaningful for cloud-image VMs; callers should skip this for bootc sources with no cidata ISO attached.

func (*SSHExecutor) WaitForPackageLock added in v0.2026190.848

func (e *SSHExecutor) WaitForPackageLock(ctx context.Context) error

WaitForPackageLock makes the guest package manager READY for a deploy plan's own `pacman -Sy` / `apt` / `dnf`: (1) it blocks until no boot-time package PROCESS is running (cloud-init's first-boot seed-package install holds the lock through cloud-final.service — the direct R4 sync primitive on the contended resource), then (2) it clears a STALE lock FILE left with no holding process. A boot-time pacman is reaped by systemd when cloud-final.service's cgroup is torn down, leaving an empty `/var/lib/pacman/db.lck` that no process owns; pacman then refuses ("unable to lock database") even though nothing is running. Removing a lock NO process holds is the standard, safe stale-lock recovery — gated on a re-check of no-process inside the command so it never races a live transaction. Process-based (not lock-file-based) for the WAIT so a stale lock can't hang the gate forever.

func (*SSHExecutor) WaitForSSH added in v0.2026190.848

func (e *SSHExecutor) WaitForSSH(ctx context.Context) error

WaitForSSH polls the guest's sshd until it accepts connections (bounded by maxWaitSeconds). Returns nil on first successful connect, error on timeout. Used by the vm deploy's PrepareVenue right after `charly vm create`.

The loop uses a wall-clock deadline (not an iteration count) and a 1-second sleep between attempts. The previous design (300 iterations with no sleep) fast-failed in ~15 seconds when each SSH attempt errored quickly — connection-refused returns in ~50 ms, host-key-mismatch in ~20 ms — burning the entire "300-second budget" in a tiny window. With a 1 s sleep between attempts, slow-to-boot guests get the full polling window they were always supposed to receive. Fixed during the 2026-05-06 R10 follow-up.

type SSHReverseRunner added in v0.2026201.1934

type SSHReverseRunner struct {
	Exec *SSHExecutor
}

SSHReverseRunner adapts an *SSHExecutor to ReverseRunner (P13-KERNEL), so a teardown replays recorded ReverseOps over SSH — inside a VM guest, or on a `local: {host: user@machine}` remote — instead of on the operator host.

func (*SSHReverseRunner) RunSystem added in v0.2026201.1934

func (r *SSHReverseRunner) RunSystem(script string) error

func (*SSHReverseRunner) RunUser added in v0.2026201.1934

func (r *SSHReverseRunner) RunUser(script string) error

type ScanSpec added in v0.2026194.1400

type ScanSpec struct {
	Path      string `yaml:"path" json:"path"`
	Recursive bool   `yaml:"recursive" json:"recursive"`
	// Manifest is the per-directory manifest filename to look for. Empty
	// defaults to UnifiedFileName; configurable per spec in charly.yml.
	Manifest string `yaml:"manifest,omitempty" json:"manifest,omitempty"`
}

ScanSpec describes one discovery root. Accepts string shorthand ("candy" → {Path: "candy", Recursive: true}) or the explicit object form ({path: X, recursive: false}). Empty Path is invalid.

func AnchorScanSpecs added in v0.2026194.1400

func AnchorScanSpecs(specs []ScanSpec, srcDir string) []ScanSpec

AnchorScanSpecs returns a copy of `specs` with every relative Path resolved to an absolute path against `srcDir`. Absolute paths are kept verbatim. Empty srcDir leaves specs unchanged so the root-file merge (called with rootDir == workspace) is a no-op.

func (*ScanSpec) UnmarshalYAML added in v0.2026194.1400

func (s *ScanSpec) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts the string shorthand where Recursive defaults to true, and the object form where Recursive defaults to true when omitted.

type ScenarioContext added in v0.2026191.1837

type ScenarioContext struct {
	// CurrentStepID is rewritten for each step as the plan run executes, so ${STEP_ID}
	// references resolve to the currently-running step's identifier. Reporters surface this
	// on failures.
	CurrentStepID string

	// Backgrounds tracks PIDs of host-side processes spawned by `command:` verbs with
	// `background: true`. Reaped at plan teardown via SIGTERM (best-effort; non-fatal on
	// failure).
	Backgrounds []int

	// Results accumulates CheckResults from steps that have completed, indexed by step ID.
	// Used by the `summarize:` verb to walk prior steps' Elapsed durations and compute
	// distribution metrics.
	Results map[string]CheckResult
	// contains filtered or unexported fields
}

ScenarioContext carries per-plan-run mutable state across the execution of that run's steps — principally the capture store populated by checks with `capture: <name>`. Instantiated fresh per plan run (and per count expansion) so cross-run state never leaks.

The struct also threads the current step identifier through variable expansion (${STEP_ID}) so artifact paths and narrative text can embed stable references without the runner having to know about them.

A ScenarioContext is OWNED by one plan run's execution pass. When that pass completes, the context is discarded; the next run gets a fresh one. `capture:` values never survive the run that produced them.

func NewScenarioContext added in v0.2026191.1837

func NewScenarioContext() *ScenarioContext

NewScenarioContext returns an empty plan-run context.

func (*ScenarioContext) AddBackground added in v0.2026191.1837

func (s *ScenarioContext) AddBackground(pid int)

AddBackground records a PID for later teardown reaping. Thread-safe.

func (*ScenarioContext) ApplyToEnv added in v0.2026191.1837

func (s *ScenarioContext) ApplyToEnv(env map[string]string)

ApplyToEnv merges plan-run-scope variables into an env map for variable expansion. Called by the runner immediately before `Check.ExpandVars(env)` so the existing `${NAME[:arg]}` grammar picks up captures and the step id without knowing about the ScenarioContext type.

Keys populated:

  • STEP_ID → ctx.CurrentStepID

ApplyToEnv overlays — it never overwrites existing keys so a host-level `${STEP_ID}` override (if ever introduced for testing) continues to win. The runner builds env by copying its resolver's base env first and then calling ApplyToEnv on the copy.

func (*ScenarioContext) SnapshotBackgrounds added in v0.2026191.1837

func (s *ScenarioContext) SnapshotBackgrounds() []int

SnapshotBackgrounds returns a copy of the current backgrounds slice. Used by the teardown reaper.

type Scope added in v0.2026190.848

type Scope = spec.Scope

type ServicePackagedDesc

type ServicePackagedDesc struct {
	Unit   string
	Enable bool
}

ServicePackagedDesc is the candy-decodable construction input for a service-packaged step: the host materializer adds the op-resolved scope + candy name and keeps the load-bearing Reverse() (disable / restore-enabled / remove-dropin) in package main.

type ShellExecutor added in v0.2026190.848

type ShellExecutor struct{}

ShellExecutor implements DeployExecutor against the invoking user's shell + filesystem. Faithful behavior-preserving wrapper around the existing runSudoShell / runUserShell / BuilderRun helpers.

func (ShellExecutor) GetFile added in v0.2026190.848

func (ShellExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)

GetFile on the local executor is a direct filesystem read. When asRoot is set, the read is delegated to `sudo cat` so files with restricted permissions (e.g. /etc/shadow, rancher kubeconfig) can still be retrieved. Stdout is captured verbatim.

func (ShellExecutor) Kind added in v0.2026190.848

func (ShellExecutor) Kind() string

Kind reports "host" — ShellExecutor's commands run on the operator's machine.

func (ShellExecutor) PutFile added in v0.2026190.848

func (ShellExecutor) PutFile(_ context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts EmitOpts) error

PutFile on the local executor is a direct filesystem write. When ownerRoot is set, the installer uses `sudo install -m <mode> -o root -g root` so the target path can be /usr/local/bin or similar.

func (ShellExecutor) ResolveHome added in v0.2026190.848

func (ShellExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` on the local host. Empty user resolves to the invoking operator's $HOME (matches today's `os.Getenv("HOME")` behaviour). Non-empty user goes through `getent passwd <user>` so callers can resolve any user's home.

func (ShellExecutor) RunBuilder added in v0.2026190.848

func (ShellExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)

RunBuilder delegates to the package-level BuilderRun.

func (ShellExecutor) RunCapture added in v0.2026190.848

func (ShellExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a shell command on the local host and returns captured stdout/stderr/exit. Mirrors the deleted ContainerExecutor / ImageExecutor / VmTestExecutor behaviour from the pre-cutover test- time interface — callers (testrun.go verbs) get the same return shape via the unified DeployExecutor interface.

func (ShellExecutor) RunInteractive added in v0.2026195.501

func (ShellExecutor) RunInteractive(_ context.Context, script string) (int, error)

func (ShellExecutor) RunStream added in v0.2026195.501

func (ShellExecutor) RunStream(_ context.Context, script string) (int, error)

func (ShellExecutor) RunSystem added in v0.2026190.848

func (s ShellExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error

RunSystem delegates to the package-level runSudoShell.

func (ShellExecutor) RunUser added in v0.2026190.848

func (s ShellExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error

RunUser delegates to the package-level runUserShell.

func (ShellExecutor) StartProcess added in v0.2026200.1312

func (ShellExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess launches argv directly on the local venue. No shell is involved.

func (ShellExecutor) Venue added in v0.2026190.848

func (ShellExecutor) Venue() string

Venue returns the fixed "local" identifier — commands always run on the invoking user's host.

type ShellKind

type ShellKind string

ShellKind classifies the venue's login shell.

const (
	ShellBash ShellKind = "bash"
	ShellZsh  ShellKind = "zsh"
	ShellFish ShellKind = "fish"
)

func DetectShellFromPath

func DetectShellFromPath(shellPath string) ShellKind

DetectShellFromPath maps a $SHELL path (or shell base name) to a ShellKind. Unknown / empty shells default to bash — the POSIX-safest choice.

type Status

type Status = spec.Status

Status is a check verdict. It is the ONE pass/fail/skip enum for the check engine and every plugin candy — charly's CheckStatus is a type alias of it. FLOOR-SLIM Unit 4: Status itself (+ the iota consts + the String() method) moved to spec.Status (sdk/spec/status_result.go) as part of the CheckResult wire-envelope split — gengotypes has no construct for an iota enum + Stringer, so CUE owns the wire VALUE SET (a plain int) and Go owns the formatting behavior there. Kept as a type ALIAS here (not a repointed reference) so kit.Result + every out-of-process plugin candy's kit.Pass/Fail/Skip/StatusPass call sites compile UNCHANGED — this is a public SDK surface with ~15 external consumers, out of scope for an internal core-floor refactor.

type StepDescriptor

type StepDescriptor struct {
	ServicePackaged *ServicePackagedDesc
	SystemPackages  *SystemPackagesDesc
}

StepDescriptor is the candy-decodable construction input for a TYPED install-plan step (the build/deploy install timeline). Exactly one variant is non-nil; the host materializer rebuilds the real package-main InstallStep from it (computing the package-main-only inputs — scope from op.RunAs+img, candy name — and keeping the load-bearing Reverse() in package main, so the candy never imports an IR type).

type StepGrader added in v0.2026191.2205

type StepGrader interface {
	Grade(ctx context.Context, req GraderRequest) CheckResult
}

StepGrader judges an agent step (agent-run:/agent-check:). The host impl spawns the configured kind:agent CLI to probe the live target and return a pass/fail verdict; nil when no grader is bound (agent steps then advisory-skip, or fail under strict).

type StepKind added in v0.2026190.848

type StepKind = spec.StepKind

type StepKindName

type StepKindName string

StepKindName names the TYPED install-plan step a step-providing verb lowers into. The host maps it to its internal StepKind enum; kept a string so the kit need not import charly's package main.

const (
	// StepKindServicePackaged — the `service` verb (enable a packaged unit; load-bearing reversals).
	StepKindServicePackaged StepKindName = "service-packaged"
	// StepKindSystemPackages — the `package` verb (install system packages).
	StepKindSystemPackages StepKindName = "system-packages"
)

type StepPass added in v0.2026194.1605

type StepPass struct {
	Stdout   string `json:"stdout,omitempty"`
	Stderr   string `json:"stderr,omitempty"`
	ExitCode int    `json:"exit_code,omitempty"`
}

StepPass is the verbatim stdout/stderr/exit-code of a host-delegated guest sub-invocation (the nested pod-in-VM check-live delegation, runVm's guestNestedCheckCmd path). The plugin writes Stdout/Stderr and returns ExitCode unchanged, so a guest-run check reports byte-identically to a direct one. Hand-written (not CUE): it is part of the kit reply model, which the wire mandate's spike keeps hand-written alongside CheckRunReply.

type StepProvider

type StepProvider interface {
	StepKind() StepKindName
	ConstructStepDescriptor(op *spec.Op) StepDescriptor
}

StepProvider is the OPTIONAL third role of a host-coupled verb candy: a verb whose build/deploy ACT lowers into a TYPED install-plan step (service → service-packaged, package → system-packages) rather than a shell (ProvisionActor) or a generic OpStep. StepKind names the target step (static); ConstructStepDescriptor returns the candy-decodable construction inputs for one op. The host wraps a candy implementing this in an adapter that satisfies package-main's TypedStepProvider, materializing the descriptor into the real IR step.

type StepRecord added in v0.2026190.848

type StepRecord struct {
	Kind        StepKind          `json:"kind"`
	Scope       Scope             `json:"scope,omitempty"`
	Venue       Venue             `json:"venue,omitempty"`
	Summary     string            `json:"summary,omitempty"`
	CompletedAt string            `json:"completed_at"`
	Extra       map[string]string `json:"extra,omitempty"`
}

StepRecord is a thin summary of a completed InstallStep that the ledger keeps for audit. Kept intentionally small — the ReverseOps list on CandyRecord is the source of truth for teardown.

type StepResult added in v0.2026191.1732

type StepResult struct {
	Keyword string      `json:"keyword"`
	Text    string      `json:"text"`
	Origin  string      `json:"origin,omitempty"`
	StepID  string      `json:"step_id"`
	Result  CheckResult `json:"result"`
}

StepResult is one plan step's outcome — the step's identity (keyword/text/origin/id) plus the CheckResult of running it. The result reporters consume a []StepResult.

func RunPlan added in v0.2026191.2205

func RunPlan(ctx context.Context, pc PlanContext, set *LabelDescriptionSet, strict bool) []StepResult

RunPlan executes the flat plan in a LabelDescriptionSet (already collected + include-expanded + overlay-merged) against the plan context, returning per-step results.

The context's mode selects which steps execute: VerifyOnly (check live / box) runs check:/agent-check: only; provision-and-verify (default) runs every step in order. include: steps never reach here (expanded at collect time); a residual one is a no-op skip. Agent steps route to the grader; run/check stamp the keyword-derived do-mode and dispatch through RunOne.

type SystemPackagesDesc

type SystemPackagesDesc struct {
	Package    string
	PackageMap map[string]string
}

SystemPackagesDesc is the candy-decodable construction input for a system-packages step (the `package` verb): the authored package name + per-distro map. The host materializer resolves the cross-distro name (ResolvePackageName against the image's tags), sets the image format + PhaseInstall, and builds the SystemPackagesStep.

type TagExpr added in v0.2026191.1413

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

TagExpr is an opaque compiled tag expression. Nil means "match everything" (no filter), so callers can write `if expr.Match(tags)` without a nil check.

func CombineTagFilters added in v0.2026191.1413

func CombineTagFilters(include, exclude *TagExpr) *TagExpr

CombineTagFilters composes an include-filter and an exclude-filter into one effective expression. Either side may be nil:

  • include nil, exclude nil → always matches
  • include X, exclude nil → matches when X is true
  • include nil, exclude Y → matches when Y is false
  • include X, exclude Y → matches when X is true AND Y is false

func ParseTagExpr added in v0.2026191.1413

func ParseTagExpr(src string) (*TagExpr, error)

ParseTagExpr compiles a tag expression. Empty / whitespace input produces a nil TagExpr that matches everything. A syntax error is returned rather than silently matching or silently failing.

func (*TagExpr) Match added in v0.2026191.1413

func (t *TagExpr) Match(tags []string) bool

Match reports whether the tag set satisfies the expression. A nil TagExpr matches everything.

func (*TagExpr) String added in v0.2026191.1413

func (t *TagExpr) String() string

String returns the raw source the expression was compiled from.

type Venue added in v0.2026190.848

type Venue = spec.Venue

type VenueResolver added in v0.2026194.1605

type VenueResolver func(venue string) (exec Executor, env map[string]string, hasRuntime bool, err error)

VenueResolver retargets a per-step venue (the `on:` modifier / venue swap) to a host executor, its resolved variable env, and whether that env carries runtime state. venue→executor is the host check_venue atom and the env is host var resolution — both stay core; Runner drives the swap through this seam. An unknown venue returns a non-nil err.

type VerbResolver added in v0.2026191.2205

type VerbResolver interface {
	// RunVerb resolves op's verb word and runs it, returning (result, true). (_, false)
	// means no such verb is registered — the walk reports the op as an unknown-verb skip.
	RunVerb(ctx context.Context, op *spec.Op) (spec.CheckResult, bool)
	// RunProvisionAct runs a do:act state-provision verb's act (create/configure) and
	// returns (result, true). (_, false) means the verb has no act path — the walk falls
	// through to the assert dispatch (the handler IS the act for action verbs).
	RunProvisionAct(ctx context.Context, op *spec.Op, verb string) (spec.CheckResult, bool)
}

VerbResolver is the verb-dispatch seam — the ONE thing the walk needs from the core provider registry. The core impl resolves the op's verb word and runs it (in-proc CheckVerbProvider fast path or out-of-process Invoke envelope), threading the live host CheckContext it owns; the walk never sees a provider type or the registry. VerbResolver's methods return spec.CheckResult (not the richer kit.CheckResult) — FLOOR-SLIM Unit 4: the host-side implementer (charly's hostVerbResolver) dispatches through the core-only provider registry down to package-main's own CheckVerbProvider family, which never sets DeadlineExceeded (an engine-internal retry signal kit's OWN dispatch loop, below, stamps AFTER the verb call returns) — so the interface boundary needs only the wire-portable base.

type VmSource added in v0.2026190.848

type VmSource = spec.VmSource

type VmSshStanza added in v0.2026186.739

type VmSshStanza struct {
	Alias          string // ssh-config Host alias (e.g. "charly-arch-vm"); unique within the fragment
	Hostname       string // IP/DNS ssh connects to ("127.0.0.1" for user-mode networking)
	Port           int    // host-side port forwarded to the guest's :22
	User           string // guest account ssh logs in as
	IdentityFile   string // absolute private-key path
	KnownHostsFile string // absolute per-VM known_hosts path
}

VmSshStanza captures the fields to render one ssh-config Host stanza for a VM.

type WalkOpts

type WalkOpts struct {
	// Shell overrides the detected venue login shell for the managed-block finalizer.
	Shell ShellKind
	// Home overrides the detected venue home. When empty WalkPlans probes `$HOME`.
	Home string
}

WalkOpts tunes the walk. All fields optional — WalkPlans probes the venue for the shell + home it needs (the env.d managed-block finalizer) when they are not supplied.

Jump to

Keyboard shortcuts

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