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
- Variables
- func AcquireFileLock(path string, blocking bool) (release func() error, err error)
- func AcquireImageBuildLock(fullTag string) (func() error, error)
- func AcquireLocalPkgBuildLock(srcDir string) (func() error, error)
- func AcquireVmDomainLock(domain string) (func() error, error)
- func AddBox(dir, name, base string, layers []string) error
- func AddCandyDeployment(paths *LedgerPaths, candyName, deployID string, update func(*CandyRecord)) error
- func AddCandyDeploymentVia(exec spec.DeployExecutor, paths *LedgerPaths, candyName, deployID string, ...) error
- func AppendUnique(dst []string, items ...string) []string
- func ApplyPortOverrides(ports []string, overrides []string) ([]string, error)
- func AtomicWriteFile(path string, data []byte, perm os.FileMode) error
- func BareVolumeName(volumeName, boxName, instance string) string
- func BedVmDomains(name string, node spec.BundleNode) []string
- func BuildArchExports() string
- func BuildBuilderRunArgs(opts BuilderRunOpts) []string
- func BuildPortMapping(boxPorts []string) map[int]int
- func BuilderCollectContext(word string, in spec.BuilderCollectInput) map[string]any
- func BuilderResolve(word string, in spec.BuilderResolveInput) (spec.BuilderResolveReply, error)
- func BuilderReverse(word string, in spec.BuilderReverseInput) []spec.ReverseOp
- func BuilderRun(ctx context.Context, opts BuilderRunOpts) ([]byte, error)
- func CheckLevelReaches(have, want string) bool
- func ClassifyContainerInfraFailure(exitCode int, stderr string) (signature string, ok bool)
- func ClassifyStepFailures(results []StepResult) (checkFails, infraFails int)
- func CloseHostCleanups(cleanups []func())
- func CollectAnyStrings(v any) []string
- func CollectHostRefs(checks []spec.Op) []string
- func CompareCalVer(a, b string) int
- func CompareSemver(a, b string) int
- func ContainerImage(engine, containerName string) string
- func ContainerImageRef(engine, containerName string) (string, error)
- func ContainerInfraError(signature string, exitCode int, stderrPreview string) error
- func ContainerName(boxName string) string
- func ContainerNameInstance(boxName, instance string) string
- func ContainerPortsFromMappings(mappings []string) []int
- func DecodeInput(in map[string]any, out any)
- func DefaultDeployConfigPath() (string, error)
- func DeleteCandyRecord(paths *LedgerPaths, layer string) error
- func DeleteDeployRecord(paths *LedgerPaths, id string) error
- func DescentFromTraits(t *spec.DeployTraits) *spec.DescentDescriptor
- func DescriptorFromExecutor(exec spec.DeployExecutor) spec.VenueDescriptor
- func DetectEngine() (string, error)
- func DetectRunMode(runEngine string) string
- func DirExists(path string) bool
- func DirectDeployMarkerDir() (string, error)
- func DirectDeployMarkerPath(box, instance string) (string, error)
- func DiscoverRemoteCandy(repoDir string) ([]string, error)
- func DiscoverSkipDir(name string) bool
- func DotenvLoaded(name string) bool
- func DownloadRepo(repoPath string, version string) (string, error)
- func EffectiveStepID(s *spec.Step, origin string, stepIdx int) string
- func EffectiveTags(stepTags []string) []string
- func EngineBinary(engine string) string
- func EnrichNoProxy(envs []string, containerNames []string) []string
- func EnsureCharlyInDeployVenue(ctx context.Context, ve spec.DeployExecutor, charlyBin, hostVer string) (string, error)
- func EnsureCharlyInGuest(ctx context.Context, ssh SSHArgs, charlyBin, hostVer, strategyRaw string) (string, error)
- func EnsureCharlyInVenue(ctx context.Context, ve DeployExecutor, charlyBin, hostVer string) (string, error)
- func EnsureSshConfigInclude(home string) error
- func EnvMapToPairs(env map[string]string) []string
- func EnvPairsToMap(pairs []string) map[string]string
- func EnvdDir(home string) string
- func EnvdFilePath(home, candyName string) string
- func ExpandAnyVars(v any, env map[string]string) (any, []string)
- func ExpandHostHome(path string) string
- func ExpandOpVars(c *spec.Op, env map[string]string) []string
- func ExpandPath(path string, home string) string
- func ExpandTestVars(s string, env map[string]string) (string, []string)
- func ExtractCalVerTag(ref string) string
- func ExtractMetadata(engine, imageRef string) (*spec.BoxMetadata, error)
- func FileExists(path string) bool
- func FillReverseUninstallCmds(ops []ReverseOp, renderUninstall func(format string, packages []string) string)
- func FilterHostVars(missing []string) []string
- func FindEntityDirs(path, filename string, recursive bool) ([]string, error)
- func FindMappingValue(m *yaml.Node, key string) *yaml.Node
- func FindPortOwner(port int, engine string) (owner string, ownerType string)
- func FirstUnmetDepStep(s spec.Step, verdictByID map[string]string) string
- func FirstYAMLVersionLine(data []byte) string
- func FormatHTTPHeaders(h http.Header) string
- func FormatPortConflicts(conflicts []PortConflict, image string) string
- func FormatPortMapping(p ParsedPortMapping) string
- func FormatStepResultsJSON(w io.Writer, results []StepResult) error
- func FormatStepResultsJUnit(w io.Writer, results []StepResult) error
- func FormatStepResultsTAP(w io.Writer, results []StepResult)
- func FormatStepResultsText(w io.Writer, results []StepResult)
- func GPURunArgs(engine string) []string
- func GitClone(repoURL string, ref string, commit string, targetDir string) error
- func GitDefaultBranch(repoURL string) (string, error)
- func GitLatestTag(repoURL string) (string, error)
- func GitResolveRef(repoURL string, ref string) (string, error)
- func HTTPClientFor(base *http.Client, req HTTPRequest) (*http.Client, error)
- func HostCharlyIsNewer(hostVer, venueVerOut string) bool
- func HumanBytes(n int64) string
- func ImageBuildLockPath(fullTag string) (string, error)
- func InputStr(c *spec.Op, key string) string
- func InspectImageLabels(engine, imageRef string) (map[string]string, error)
- func InstallDirAtomic(tmp, final string) error
- func IsAutoPort(mapping string) bool
- func IsContainerInfraResult(message string) bool
- func IsDirectDeploy(box, instance string) bool
- func IsGitSubmoduleDir(p, root string) bool
- func IsHostNetworked(engine, containerName string) bool
- func IsMutableRef(version string) bool
- func IsRemoteImageRef(ref string) bool
- func IsRepoCached(repoPath, version string) (bool, error)
- func IsRuntimeOnlyVar(key string) bool
- func IsValidCheckLevel(level string) bool
- func KeywordOf(s *spec.Step) spec.StepKeyword
- func ListVmSshAliases(home string) ([]string, error)
- func LoadProcessDotenv(dir string) error
- func LoadWorkspaceEnv(workspace string) ([]string, error)
- func LooksLikeFullRef(ref string) bool
- func ManagedBlockBody(shell ShellKind, home string) string
- func ManagedBody(text string) string
- func MapValue(m *yaml.Node, key string) *yaml.Node
- func MappingChild(m *yaml.Node, key string) *yaml.Node
- func MarkersForTag(marker string) (begin, end string)
- func MigrateCandidateYAMLFiles(dir string, treeSubdirs []string) []string
- func NestedContainerName(path string) string
- func NodeShapedValue(val *yaml.Node) bool
- func NormalizeTag(t string) string
- func OpUnifyCandidateFiles(dir string) []string
- func ParseContainerPort(mapping string) (int, error)
- func ParseEnvBytes(data []byte) ([]string, error)
- func ParseEnvFile(path string) ([]string, error)
- func ParseHostPort(mapping string) (int, error)
- func ParseHostPorts(boxPorts []string) []int
- func ParsePublishedPort(output string, port int) (string, error)
- func ParseTaskMode(mode string, def uint32) uint32
- func PluginPrimaryFor(word string) (string, bool)
- func Plural(n int) string
- func PodName(boxName string) string
- func PodNameInstance(boxName, instance string) string
- func PodQuadletFilename(boxName string) string
- func PodQuadletFilenameInstance(boxName, instance string) string
- func PosArtifact(c *spec.Op) []string
- func PosCommandFields(c *spec.Op) []string
- func PosKeyNameSplit(c *spec.Op) []string
- func PosLibvirtQmp(c *spec.Op) []string
- func PosTarget(c *spec.Op) []string
- func PosText(c *spec.Op) []string
- func QuadletDir() (string, error)
- func QuadletExists(boxName string) (bool, error)
- func QuadletExistsInstance(boxName, instance string) (bool, error)
- func QuadletFilename(boxName string) string
- func QuadletFilenameInstance(boxName, instance string) string
- func ReexecOverSSH(host, identityFile string, options []string, controllerBin, version string, ...) int
- func RegisterPluginPrimary(word, field string) error
- func RemoteLaunchCommand(launch spec.ProcessLaunch) string
- func RemoveImagesByReference(engineBin, reference string)
- func RemoveManagedBlockAt(path, marker string) error
- func RemoveSshConfigInclude(home string) error
- func RemoveVmSshStanza(home string, alias string) (remaining int, err error)
- func RenderDownloadScript(op *spec.Op, candyVars map[string]string) string
- func RenderEnvdBody(candyName string, envVars map[string]string, pathAdd []string) string
- func RenderManagedBlockStrip(path, marker string) string
- func RenderOpCommand(op *spec.Op, ctxPath string, candyVars map[string]string) (string, bool)
- func ReplaceOrAppendManagedBlock(existing, body, marker string) string
- func ReplaceOrPrependManagedBlock(existing, body, marker string) string
- func RepoCacheDir() (string, error)
- func RepoCachePath(repoPath, version string) (string, error)
- func RepoGitURL(repoPath string) string
- func ReportStepResults(w io.Writer, results []StepResult, format string)
- func ReportStepResultsCount(w io.Writer, results []StepResult, format string) int
- func ResolveApkPath(ref, candyDir string) (string, error)
- func ResolveAutoEnable(envVal string, cfgVal *bool) bool
- func ResolveBoxName(box string) string
- func ResolveCheckLevel(level string) string
- func ResolveDeployPorts(containerPorts []int, pins, prior []string, occupied map[int]bool) ([]string, error)
- func ResolveEncryptedStoragePath(envVal, cfgVal string) string
- func ResolveEntrypointFromMeta(meta *spec.BoxMetadata) []string
- func ResolveEnvVars(globalEnv []string, deployEnv []string, deployEnvFile string, envDir string, ...) ([]string, error)
- func ResolveInitDefFromMeta(meta *spec.BoxMetadata) (*spec.ResolvedInit, error)
- func ResolveLocalImageRef(engine, input string) (string, error)
- func ResolveNetwork(configured, engine string) (string, error)
- func ResolveNewestLocalCalVer(engine, short string) (string, error)
- func ResolvePackageName(pkg string, packageMap map[string]string, distros []string) string
- func ResolveProto(containerPort int, portProtos map[string]string) string
- func ResolveShellImageRef(registry, name, tag string) string
- func ResolveValue(envVal, cfgVal, defaultVal string) string
- func ResolveVmSshPort(vm *spec.ResolvedVm, vmName string, persistedPort int) (int, error)
- func ResolveVolumesPath(envVal, cfgVal string) string
- func ResugarPlan(plan *yaml.Node)
- func RunAgentOnce(ctx context.Context, ai *spec.AgentExecSpec, prompt string, ...) (string, string, error)
- func RunCaptureCmd(cmd *exec.Cmd) (string, string, int, error)
- func RunReverseOps(ops []ReverseOp, exec ReverseExecutor)
- func SameStringSlice(a, b []string) bool
- func SanitizeDeployName(s string) string
- func SaveRuntimeConfig(cfg *RuntimeConfig) error
- func SaveYAMLNodeFile(path string, root *yaml.Node) error
- func ScaffoldCandy(dir, name, calver string) error
- func ScaffoldProject(dir string) error
- func ServiceName(boxName string) string
- func ServiceNameInstance(boxName, instance string) string
- func SetByDotPath(path, dotpath, valueYAML string) error
- func ShDoubleQuote(v string) string
- func ShDoubleQuotePath(v string) string
- func ShQuoteArg(v string) string
- func ShQuoteEnv(v string) string
- func ShellInitFilePath(shell ShellKind, home string) string
- func ShellQuote(s string) string
- func ShutdownProcessGroup(cmd *exec.Cmd, stdin io.Closer, done <-chan struct{})
- func SidecarConfigDir() (string, error)
- func SidecarContainerName(boxName, sidecarName string) string
- func SidecarContainerNameInstance(boxName, instance, sidecarName string) string
- func SidecarQuadletFilename(boxName, sidecarName string) string
- func SidecarQuadletFilenameInstance(boxName, instance, sidecarName string) string
- func SortStrings(s []string)
- func SortedEnvPairs(env map[string]string) []string
- func SplitHostKey(key string) (name, arg string, ok bool)
- func SshConfigPath(home string) string
- func SshFragmentPath(home string) string
- func StampDescent(node *spec.Deploy, traitsFor func(word string) *spec.DeployTraits)
- func StepDoMode(s *spec.Step) spec.DoMode
- func StepID(origin string, stepIdx int) string
- func StripManagedBlock(existing, marker string) string
- func StripPortSuffix(s string) (string, string)
- func StripURLScheme(ref string) string
- func SystemdUserAvailable() bool
- func SystemdUserDir() (string, error)
- func TaskShellPreamble(candyVars, opEnv map[string]string) string
- func TestVarRefs(s string) []string
- func TransferImage(srcEngine, dstEngine, imageRef string) error
- func TransferToRootful(imageRef string) error
- func TrimPreview(s string) string
- func TunnelConfigFromMetadata(meta *spec.BoxMetadata) *spec.TunnelConfig
- func UnixToTCPBridge(socketPath string) (net.Listener, error)
- func UserScopeBindMounts(hostHome string) (map[string]string, error)
- func UserScopeEnv(hostHome string) map[string]string
- func ValidateBindAddress(value string) error
- func ValidateEngine(value, field string) error
- func ValidatePlanSteps(desc string, plan []spec.Step, eid string) []string
- func ValidateRunMode(value string) error
- func ValidateTagExpr(tag string) error
- func VenueFromDescriptor(d spec.VenueDescriptor) (spec.DeployExecutor, error)
- func VmSshAlias(vmName string) string
- func WaitForCloudInit(ctx context.Context, ssh SSHArgs, poll PollFunc) error
- func WaitForPackageLock(ctx context.Context, ssh SSHArgs, poll PollFunc) error
- func WaitForSSH(ctx context.Context, ssh SSHArgs, poll PollFunc) error
- func WalkPlans(ctx context.Context, exec DeployExecutor, plans []spec.InstallPlanView, ...) ([]spec.ReverseOp, error)
- func WrapContainerCommand(script string) string
- func WriteCandyRecord(paths *LedgerPaths, rec *CandyRecord) error
- func WriteDeployRecord(paths *LedgerPaths, rec *DeployRecord) error
- func WriteDeployRecordVia(exec spec.DeployExecutor, paths *LedgerPaths, rec *DeployRecord) error
- func WriteVmSshStanza(home string, s VmSshStanza) error
- type AgentForwardMounts
- type AgentGrader
- type BuilderRunOpts
- type CalVer
- type CandyRecord
- type CharlyInstallStrategy
- type CheckContext
- type CheckResult
- type CheckRunReply
- type CheckVarResolver
- type CheckVerbProvider
- type ContainerInspection
- type DefaultDownloader
- type DeployExecutor
- type DeployRecord
- type DiscoverConfig
- type DocShape
- type EmitOpts
- type EngineConfig
- type EnvConfig
- type Executor
- type FetchedImage
- type GraderRequest
- type GraphicsEndpoint
- type HTTPRequest
- type HTTPResponse
- type ImportEntry
- type ImportList
- type InspectConfig
- type InspectHostConfig
- type InspectMount
- type InspectNetwork
- type InspectNetworkBind
- type InspectPortBind
- type JumpKind
- type LabelDescriptionSet
- type LabeledDescription
- type LedgerLock
- type LedgerPaths
- type LocalImageInfo
- type MethodSpec
- type NestedExecutor
- func (n *NestedExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)
- func (n *NestedExecutor) Kind() string
- func (n *NestedExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, ...) error
- func (n *NestedExecutor) ResolveHome(ctx context.Context, user string) (string, error)
- func (n *NestedExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)
- func (n *NestedExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)
- func (n *NestedExecutor) RunInteractive(ctx context.Context, script string) (int, error)
- func (n *NestedExecutor) RunStream(ctx context.Context, script string) (int, error)
- func (n *NestedExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error
- func (n *NestedExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error
- func (n *NestedExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)
- func (n *NestedExecutor) Venue() string
- type NestedJump
- type ParsedPortMapping
- type ParsedRef
- type PlanContext
- type PlanGrammar
- type PollCond
- type PollCondition
- type PollFunc
- type PortConflict
- type ProvisionActor
- type RefsDownloader
- type ResolvedReadiness
- type ResolvedRuntime
- type Result
- type ReverseExecutor
- type ReverseOp
- type ReverseOpKind
- type ReverseRunner
- type RunMode
- type Runner
- func (r *Runner) Box() string
- func (r *Runner) CandyDirs() map[string]string
- func (r *Runner) CandyScanErr() error
- func (r *Runner) ContextSkipReason(op *spec.Op) string
- func (r *Runner) DialTimeout() time.Duration
- func (r *Runner) Distros() []string
- func (r *Runner) EffectiveDo(op *spec.Op) spec.DoMode
- func (r *Runner) EffectiveEnv() map[string]string
- func (r *Runner) Exec() Executor
- func (r *Runner) Grader() StepGrader
- func (r *Runner) HTTPClient() *http.Client
- func (r *Runner) HasRuntime() bool
- func (r *Runner) Instance() string
- func (r *Runner) Mode() RunMode
- func (r *Runner) ProbeNeverHang(op *spec.Op) time.Duration
- func (r *Runner) Run(ctx context.Context, checks []spec.Op) []CheckResult
- func (r *Runner) Scenario() *ScenarioContext
- func (r *Runner) SetScenario(sc *ScenarioContext)
- func (r *Runner) SkipDeterministicRun() bool
- func (r *Runner) SwapVenue(op *spec.Op) (func(), string)
- func (r *Runner) Verbs() VerbResolver
- func (r *Runner) VerifyOnly() bool
- func (r *Runner) VmName() string
- func (r *Runner) VmTargetName() string
- type RunnerConfig
- type RuntimeConfig
- type RuntimeVmConfig
- type SSHArgs
- type SSHExecutor
- func (e *SSHExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)
- func (e *SSHExecutor) Kind() string
- func (e *SSHExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, ...) error
- func (e *SSHExecutor) ResolveHome(ctx context.Context, user string) (string, error)
- func (e *SSHExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)
- func (e *SSHExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)
- func (e *SSHExecutor) RunInteractive(_ context.Context, script string) (int, error)
- func (e *SSHExecutor) RunStream(_ context.Context, script string) (int, error)
- func (e *SSHExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error
- func (e *SSHExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error
- func (e *SSHExecutor) SSHBaseArgs() []string
- func (e *SSHExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)
- func (e *SSHExecutor) Venue() string
- func (e *SSHExecutor) WaitForCloudInit(ctx context.Context) error
- func (e *SSHExecutor) WaitForPackageLock(ctx context.Context) error
- func (e *SSHExecutor) WaitForSSH(ctx context.Context) error
- type SSHReverseRunner
- type ScanSpec
- type ScenarioContext
- type Scope
- type ServicePackagedDesc
- type ShellExecutor
- func (ShellExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)
- func (ShellExecutor) Kind() string
- func (ShellExecutor) PutFile(_ context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, ...) error
- func (ShellExecutor) ResolveHome(ctx context.Context, user string) (string, error)
- func (ShellExecutor) RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)
- func (ShellExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)
- func (ShellExecutor) RunInteractive(_ context.Context, script string) (int, error)
- func (ShellExecutor) RunStream(_ context.Context, script string) (int, error)
- func (s ShellExecutor) RunSystem(ctx context.Context, script string, opts EmitOpts) error
- func (s ShellExecutor) RunUser(ctx context.Context, script string, opts EmitOpts) error
- func (ShellExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)
- func (ShellExecutor) Venue() string
- type ShellKind
- type Status
- type StepDescriptor
- type StepGrader
- type StepKind
- type StepKindName
- type StepPass
- type StepProvider
- type StepRecord
- type StepResult
- type SystemPackagesDesc
- type TagExpr
- type Venue
- type VenueResolver
- type VerbResolver
- type VmSource
- type VmSshStanza
- type WalkOpts
Constants ¶
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.
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 )
const ( StatusPass = spec.StatusPass StatusFail = spec.StatusFail StatusSkip = spec.StatusSkip )
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 )
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`.
const ( KwRun spec.StepKeyword = "run" KwCheck spec.StepKeyword = "check" KwAgentRun spec.StepKeyword = "agent-run" KwAgentCheck spec.StepKeyword = "agent-check" KwInclude spec.StepKeyword = "include" )
const CharlyNetworkName = "charly"
CharlyNetworkName is the shared bridge network used by all charly containers.
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).
const DefaultCheckLevel = CheckLevelNoAgent
DefaultCheckLevel is the rung applied when a box declares no check_level.
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.
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.
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).
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`.
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".)
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.
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).
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 ¶
var ClassifyDoc = spec.ClassifyDoc
ClassifyDoc inspects a document's top level and returns its shape. See spec.ClassifyDoc.
var ContainerRunning = defaultContainerRunning
ContainerRunning reports whether a container is running. Package-level var for testability (tests inject a stub, same pattern as EnsureCharlyNetwork/InspectLabels).
var EnsureCharlyNetwork = defaultEnsureCharlyNetwork
EnsureCharlyNetwork creates the "charly" network if it does not exist. It is a package-level var for testability.
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).
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.
var InspectContainer = defaultInspectContainer
InspectContainer is swappable for tests. Real calls shell out to `<engine> inspect <name>` (which returns a one-element JSON array).
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).
var ListLocalImages = defaultListLocalImages
ListLocalImages returns all images in the engine's local storage. Package-level var for testability (same pattern as LocalImageExists, DetectGPU).
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).
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).
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.
var (
PollRemote = vmshared.PollRemote
)
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).
var RuntimeConfigPath = defaultRuntimeConfigPath
RuntimeConfigPath returns the path to the user's runtime config file.
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.
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.
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).
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().
var TestVarRefPattern = regexp.MustCompile(`\$\{([A-Z_][A-Z0-9_]*)(?::([^}]+))?\}`)
TestVarRefPattern matches a ${NAME} or ${NAME:arg} reference (uppercase-underscore names).
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 ¶
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
AcquireImageBuildLock takes the blocking per-image build lock for fullTag.
func AcquireLocalPkgBuildLock ¶ added in v0.2026190.848
AcquireLocalPkgBuildLock serializes concurrent localpkg builds sharing a source dir.
func AcquireVmDomainLock ¶ added in v0.2026201.1934
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
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
AppendUnique appends items to dst, skipping any already present in dst.
func ApplyPortOverrides ¶ added in v0.2026190.848
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
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
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
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
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
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
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
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
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
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
ContainerImage returns the image ref for a running container, best-effort ("" on error). Thin wrapper over ContainerImageRef.
func ContainerImageRef ¶ added in v0.2026198.358
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
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
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
ContainerNameInstance returns the container name with an optional instance suffix.
func ContainerPortsFromMappings ¶ added in v0.2026190.848
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 ¶
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
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
DetectEngine auto-detects the container engine: prefers podman, falls back to docker.
func DetectRunMode ¶ added in v0.2026190.848
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 DirectDeployMarkerDir ¶ added in v0.2026198.345
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
DirectDeployMarkerPath returns the marker JSON path for a deploy.
func DiscoverRemoteCandy ¶ added in v0.2026192.1026
DiscoverRemoteCandy returns the list of candy names in a remote repo directory
func DiscoverSkipDir ¶ added in v0.2026194.1400
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
DotenvLoaded reports whether a given env var name was loaded from the project .env file.
func DownloadRepo ¶ added in v0.2026192.1026
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
EffectiveStepID returns the step's author id when set, else a derived id.
func EffectiveTags ¶ added in v0.2026191.1413
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 EnrichNoProxy ¶ added in v0.2026190.848
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
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
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
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 EnvdFilePath ¶
EnvdFilePath returns the env file path for a given candy under a home.
func ExpandAnyVars ¶ added in v0.2026191.2033
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
ExpandHostHome expands ~ and $HOME in a path using the actual user's home directory.
func ExpandOpVars ¶ added in v0.2026191.2033
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
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
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
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 ¶
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
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
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 ¶
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
FindPortOwner checks running containers to identify what is using a port.
func FirstUnmetDepStep ¶ added in v0.2026201.1934
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 ¶
FirstYAMLVersionLine extracts the value of the first top-level `version:` line.
func FormatHTTPHeaders ¶ added in v0.2026197.1320
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 GitClone ¶ added in v0.2026192.1026
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
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
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
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
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
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
HumanBytes renders a byte count as a compact human-readable string.
func ImageBuildLockPath ¶ added in v0.2026193.1308
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
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
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
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
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
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
IsDirectDeploy reports whether the named deploy was created in direct mode (i.e. has a marker file). Used by lifecycle commands.
func IsGitSubmoduleDir ¶
IsGitSubmoduleDir reports whether p (≠ root) contains a .git entry (a nested submodule/repo boundary).
func IsHostNetworked ¶ added in v0.2026198.345
IsHostNetworked checks if a running container uses --network host.
func IsMutableRef ¶ added in v0.2026201.852
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
IsRemoteImageRef returns true if a ref looks like a remote image reference (starts with @)
func IsRepoCached ¶ added in v0.2026192.1026
IsRepoCached checks if a repo version is already in the cache.
func IsRuntimeOnlyVar ¶ added in v0.2026191.2033
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
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
ListVmSshAliases returns the alias names in the managed fragment, sorted.
func LoadProcessDotenv ¶ added in v0.2026190.848
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
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
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 ¶
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
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 MappingChild ¶ added in v0.2026186.1019
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 ¶
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 ¶
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
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 ¶
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
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 ¶
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
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
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
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
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
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
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 ¶
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
PluginPrimaryFor returns word's declared primary input field.
func Plural ¶ added in v0.2026191.1732
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
PodName returns the container name for a pod's primary container.
func PodNameInstance ¶ added in v0.2026198.358
PodNameInstance returns the container name for a pod's primary container, instance-aware.
func PodQuadletFilename ¶ added in v0.2026201.1135
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
PodQuadletFilenameInstance returns the quadlet filename for a pod with optional instance.
func PosArtifact ¶
func PosCommandFields ¶
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 ¶
PosKeyNameSplit splits the input `key` on whitespace (libvirt send-key: "ctrl alt F2" → 3 slots).
func PosLibvirtQmp ¶
PosLibvirtQmp emits a QMP method name + optional JSON args (input text = method, input = JSON).
func QuadletDir ¶ added in v0.2026201.1135
QuadletDir returns the user-level quadlet directory.
func QuadletExists ¶ added in v0.2026201.1135
QuadletExists checks whether a .container file exists for the given image.
func QuadletExistsInstance ¶ added in v0.2026201.1135
QuadletExistsInstance checks whether a .container file exists for the given image/instance.
func QuadletFilename ¶ added in v0.2026201.1135
QuadletFilename returns the quadlet filename for an image.
func QuadletFilenameInstance ¶ added in v0.2026201.1135
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
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
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
RemoveSshConfigInclude removes the managed Include line from ~/.ssh/config. Idempotent.
func RemoveVmSshStanza ¶ added in v0.2026186.739
RemoveVmSshStanza drops the named alias and returns the remaining count (0 → the caller also removes the Include). Idempotent.
func RenderDownloadScript ¶
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 ¶
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
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 ¶
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 ¶
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
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
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
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
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
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 ResolveBoxName ¶ added in v0.2026198.345
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
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 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
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
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
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 ¶
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
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
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
ResolveValue returns the first non-empty value from the chain.
func ResolveVmSshPort ¶ added in v0.2026201.1934
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 ResugarPlan ¶ added in v0.2026198.345
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 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
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 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
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
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
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
ServiceName returns the systemd service name for an image.
func ServiceNameInstance ¶ added in v0.2026201.1135
ServiceNameInstance returns the systemd service name for an image with optional instance.
func SetByDotPath ¶ added in v0.2026186.1019
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 ¶
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 ¶
ShDoubleQuotePath escapes a PATH-list value for use INSIDE double quotes, leaving `$` unescaped so `$PATH` expands at sourcing time.
func ShQuoteArg ¶
ShQuoteArg single-quotes an argument for POSIX shell embedding.
func ShQuoteEnv ¶
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 ¶
ShellInitFilePath returns the init file the managed block lands in for each shell under a home.
func ShellQuote ¶
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
ShutdownProcessGroup stops one Setpgid child process gracefully-first and reaps it. The escalation ladder is:
- close stdin — the polite EOF signal every stdio-carried protocol answers by exiting on its own;
- one ProcessShutdownGrace for the process to exit;
- SIGTERM to the whole process group — the explicit termination request, so a well-behaved child can still shut down cleanly;
- a second ProcessShutdownGrace;
- 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
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
SidecarContainerName returns the container name for a named sidecar.
func SidecarContainerNameInstance ¶ added in v0.2026198.345
SidecarContainerNameInstance returns the container name for a named sidecar, instance-aware.
func SidecarQuadletFilename ¶ added in v0.2026201.1135
SidecarQuadletFilename returns the quadlet filename for a sidecar container.
func SidecarQuadletFilenameInstance ¶ added in v0.2026201.1135
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
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
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
SshConfigPath returns ~/.ssh/config for the user's home dir.
func SshFragmentPath ¶ added in v0.2026186.739
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
StepDoMode maps the step keyword to the act/assert/instruct dispatch enum.
func StepID ¶ added in v0.2026191.2205
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 ¶
StripManagedBlock removes the begin/end fence pair (tagged with marker) and its body.
func StripPortSuffix ¶ added in v0.2026190.848
StripPortSuffix removes /tcp or /udp protocol suffix from a port string. "47998/udp" -> "47998", "udp"; "8000" -> "8000", ""
func StripURLScheme ¶ added in v0.2026198.345
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
SystemdUserDir returns the user-level systemd unit directory (~/.config/systemd/user/).
func TaskShellPreamble ¶
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
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
TransferImage pipes an image from one engine to another via save | load.
func TransferToRootful ¶ added in v0.2026190.848
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 ¶
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
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
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
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 ValidateEngine ¶ added in v0.2026190.848
func ValidatePlanSteps ¶ added in v0.2026197.1320
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 ValidateTagExpr ¶ added in v0.2026201.1934
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
VmSshAlias returns the canonical alias for a VM deployment name ("charly-" namespaced).
func WaitForCloudInit ¶ added in v0.2026186.739
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
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
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 ¶
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
func (g *AgentGrader) Grade(ctx context.Context, req GraderRequest) CheckResult
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 ¶
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 ¶
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 ¶
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.
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).
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 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
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
ExpandEnvConfig expands all ~ and $HOME references in an EnvConfig
func MergeEnvConfigs ¶ added in v0.2026190.848
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
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 ¶
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
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
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
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 (*NestedExecutor) RunSystem ¶ added in v0.2026190.848
RunSystem routes a bash script through the jump as root.
func (*NestedExecutor) RunUser ¶ added in v0.2026190.848
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
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
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
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.
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 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 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
Box / Instance are the deployment's image + instance names (empty under ModeBox).
func (*Runner) CandyScanErr ¶ added in v0.2026194.1605
func (*Runner) ContextSkipReason ¶ added in v0.2026194.1605
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
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
Distros is the image's distro tag list, for the exclude_distros: skip.
func (*Runner) EffectiveDo ¶ added in v0.2026194.1605
EffectiveDo resolves op's do-mode via the injected grammar (DoAssert when no grammar is wired).
func (*Runner) EffectiveEnv ¶ added in v0.2026194.1605
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
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 (*Runner) HasRuntime ¶ added in v0.2026194.1605
func (*Runner) ProbeNeverHang ¶ added in v0.2026194.1605
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
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
SkipDeterministicRun skips deterministic run: install-timeline steps (feature-run mode).
func (*Runner) SwapVenue ¶ added in v0.2026194.1605
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
VerifyOnly restricts the walk to idempotent verify steps (check:/agent-check:).
func (*Runner) VmName ¶ added in v0.2026194.1605
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
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
SSHArgs is the host-surface ssh/scp coordinates for a managed VM alias.
func (SSHArgs) BaseArgs ¶ added in v0.2026186.739
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
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
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
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 (*SSHExecutor) RunSystem ¶ added in v0.2026190.848
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
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
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.
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 ServicePackagedDesc ¶
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
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
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) RunSystem ¶ added in v0.2026190.848
RunSystem delegates to the package-level runSudoShell.
func (ShellExecutor) RunUser ¶ added in v0.2026190.848
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.
func DetectShellFromPath ¶
DetectShellFromPath maps a $SHELL path (or shell base name) to a ShellKind. Unknown / empty shells default to bash — the POSIX-safest choice.
type 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 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 ¶
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
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
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.
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 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.
Source Files
¶
- agent_forward.go
- apk_path.go
- append_unique.go
- atomic_write.go
- box_metadata.go
- builder.go
- builder_resolve.go
- builder_run.go
- calver.go
- calver_compare.go
- charly_install.go
- checklevel.go
- checkresult.go
- checkrun_seam.go
- checkvars.go
- checkvars_expand.go
- container_image.go
- container_name.go
- container_probe.go
- deploy_executor.go
- deploy_executor_live.go
- deploy_executor_nested.go
- deploy_executor_process.go
- deploy_executor_ssh.go
- deploy_host_shell.go
- deploy_name.go
- deploy_types.go
- deployconfig.go
- descent.go
- description_merge.go
- direct_deploy.go
- engine.go
- entrypoint.go
- env.go
- envfile.go
- eventually.go
- filelock.go
- grader.go
- hostrefs.go
- http_do.go
- http_fetch.go
- images.go
- infra_classify.go
- install_dir.go
- install_ledger.go
- kit.go
- loader_classify.go
- loader_directives.go
- loader_discover.go
- local_image.go
- methodspec.go
- migrate_helpers.go
- migrate_support.go
- network.go
- planrun.go
- planspec.go
- planvalidate.go
- plugin_primary.go
- ports.go
- process.go
- profile.go
- quadlet_paths.go
- readiness.go
- reexec_ssh.go
- refs_cache.go
- refs_downloader.go
- refs_git.go
- remote_ref.go
- render.go
- report.go
- reverse_ops.go
- runner.go
- runtime_config.go
- scaffold.go
- scenario.go
- sidecar_naming.go
- ssh_wait.go
- sshconfig.go
- step_deps.go
- step_keyword.go
- tagexpr.go
- transfer.go
- tunnel_metadata.go
- venue_descriptor.go
- vm_domain_lock.go
- vm_ssh_port.go
- vnc_bridge.go
- walk.go
- yaml.go