deploykit

package
v0.2026211.606 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

quadlet.go — the POD config-WRITE mechanism, relocated out of charly core (P11a). GenerateQuadlet + its pure closure render a systemd quadlet .container file from a QuadletConfig; the compiled-in candy/plugin-deploy-pod builds the QuadletConfig from the host config-resolve seam and calls this. A PURE render mechanism (M under the kernel/plugin boundary law): no host I/O, no globals — byte-identical to the former core generateQuadlet (S4 parity spike). VolumeMount is a resolved runtime type owned in this package (deploy_volume.go); the tunnel wire types are spec.TunnelConfig/TunnelPort (CUE-sourced, P11 wire-mandate repair); SecurityConfig is vmshared's; ResolvedSidecar is the host-adapted deploykit runtime type (defined below), not spec's wire ResolvedSidecar.

quadlet_pod.go — the pod + sidecar quadlet content generators, relocated from charly core (P11 config-write move); a compiled-in deploy:pod plugin calls them to write the .pod + sidecar .container files.

Package deploykit re-exports the InstallPlan step VOCABULARY that now lives in spec (#55 step-4): the 13 concrete InstallStep implementations + their field-structs + the pure classification helpers. They RELOCATED to github.com/opencharly/spec/spec (install_step_vocab.go) — in-proc IR, hand-written beside the InstallStep interface + InstallPlan container it belongs to (its wire form is the CUE-sourced InstallStepView). The thin aliases below let deploykit's own code + out-of-tree plugins read unchanged; deploykit imports spec.

tunnel_resolve.go — the tunnel-config RESOLUTION mechanism, relocated out of charly core (FLOOR-SLIM mechanical batch). ResolveTunnelConfig / TunnelConfigFromMetadata turn a charly.yml TunnelYAML (or image-label metadata) into a ready-to-execute spec.TunnelConfig; parseHostPorts / buildPortMapping / resolveProto are their pure helpers. Every dependency is already sdk-portable (spec.TunnelYAML/TunnelConfig/TunnelPort/BoxMetadata, kit.ParsePortMapping) — a PURE resolve-to-envelope Mechanism under the kernel/plugin boundary law, no host I/O, no globals. charly core aliases these (tunnel.go) for its remaining pod-config-* seam call sites.

Index

Constants

View Source
const (
	VenueLocal = kit.VenueLocal

	JumpPodmanExec   = kit.JumpPodmanExec
	JumpDockerExec   = kit.JumpDockerExec
	JumpSSH          = kit.JumpSSH
	JumpVirshConsole = kit.JumpVirshConsole
)
View Source
const (
	RenderSeamInlineBuilder  = "inline-builder"
	RenderSeamEnsureBuilders = "ensure-builders"
)

RenderSeam method discriminators (the RenderSeamRequest.Method values).

View Source
const (
	PreemptStopShutdown  = spec.PreemptStopShutdown
	PreemptRestoreAlways = spec.PreemptRestoreAlways
)
View Source
const (
	ScopeSystem      = spec.ScopeSystem
	ScopeUser        = spec.ScopeUser
	ScopeUserProfile = spec.ScopeUserProfile

	PhasePrepare = spec.PhasePrepare
	PhaseInstall = spec.PhaseInstall
	PhaseCleanup = spec.PhaseCleanup

	VenueHostNative       = spec.VenueHostNative
	VenueContainerBuilder = spec.VenueContainerBuilder
	VenueSkip             = spec.VenueSkip

	GateNone             = spec.GateNone
	GateAllowRepoChanges = spec.GateAllowRepoChanges
	GateAllowRootTasks   = spec.GateAllowRootTasks
	GateWithServices     = spec.GateWithServices

	StepKindSystemPackages  = spec.StepKindSystemPackages
	StepKindBuilder         = spec.StepKindBuilder
	StepKindOp              = spec.StepKindOp
	StepKindFile            = spec.StepKindFile
	StepKindServicePackaged = spec.StepKindServicePackaged
	StepKindServiceCustom   = spec.StepKindServiceCustom
	StepKindShellHook       = spec.StepKindShellHook
	StepKindShellSnippet    = spec.StepKindShellSnippet
	StepKindRepoChange      = spec.StepKindRepoChange
	StepKindApkInstall      = spec.StepKindApkInstall
	StepKindLocalPkgInstall = spec.StepKindLocalPkgInstall
	StepKindReboot          = spec.StepKindReboot
	StepKindExternalPlugin  = spec.StepKindExternalPlugin

	ExternalStepKindPrefix = spec.ExternalStepKindPrefix

	ReverseOpCoprDisable    = spec.ReverseOpCoprDisable
	ReverseOpPackageRemove  = spec.ReverseOpPackageRemove
	ReverseOpRemoveDropin   = spec.ReverseOpRemoveDropin
	ReverseOpRemoveEnvdFile = spec.ReverseOpRemoveEnvdFile
	ReverseOpRemoveManaged  = spec.ReverseOpRemoveManaged
	ReverseOpRemoveRepoFile = spec.ReverseOpRemoveRepoFile
	ReverseOpRestoreEnabled = spec.ReverseOpRestoreEnabled
	ReverseOpRmFileSystem   = spec.ReverseOpRmFileSystem
	ReverseOpRmFileUser     = spec.ReverseOpRmFileUser
	ReverseOpServiceDisable = spec.ReverseOpServiceDisable
	ReverseOpServiceRemove  = spec.ReverseOpServiceRemove
)
View Source
const HomeToken = spec.HomeToken

HomeToken is the deferred-home placeholder resolved by ResolveHome at emit time.

View Source
const SeederHelperImage = "docker.io/library/busybox:stable"

SeederHelperImage is a small runnable image used to copy data from a FROM-scratch data image into a named volume. Pulled lazily on first use. busybox:stable has cp + sh and is ~2MB.

Variables

View Source
var (
	ExpandPath = kit.ExpandPath

	KwRun = kit.KwRun
)
View Source
var (
	// DeployKey builds the charly.yml deployment map key from a box name + optional instance.
	DeployKey = spec.DeployKey
	// ParseDeployKey is the inverse of DeployKey.
	ParseDeployKey = spec.ParseDeployKey
	// BundleDelArgv is the single `charly bundle del <name>` argv builder.
	BundleDelArgv = spec.BundleDelArgv
	// DeriveDeploymentName is the shared default-name derivation for a source-less from-box deploy.
	DeriveDeploymentName = spec.DeriveDeploymentName
)

Pure deploy-key / argv / name helpers (stdlib-only, spec-owned).

View Source
var (
	VmSshAlias = kit.VmSshAlias

	// AppendUnique — deploykit's own security.go merge logic + charly core's
	// (deploykit-consuming) call sites both use this unqualified/via `deploykit.`
	// (R3: security.go carried its own byte-identical copy of this helper under a
	// divergent name before this alias — see CHANGELOG).
	AppendUnique = kit.AppendUnique
)
View Source
var (
	WireView     = spec.WireView
	PlanFromView = spec.PlanFromView
	ResolveHome  = spec.ResolveHome
	GateEnabled  = spec.GateEnabled
)
View Source
var (
	StepToView   = spec.StepToView
	StepFromView = spec.StepFromView
	StepsToView  = spec.StepsToView
)
View Source
var (
	OpStepScope        = spec.OpStepScope
	PathIsSystemScoped = spec.PathIsSystemScoped
	IsExternalStepKind = spec.IsExternalStepKind
)
View Source
var AllStepKinds = spec.AllStepKinds

The fixed step-kind vocabulary + the pure classification helpers relocated to spec.

View Source
var AskPassword = DefaultAskPassword

AskPassword prompts for a password using systemd-ask-password. id is a unique identifier for kernel keyring caching, prompt is shown to the user. Package-level var for testability.

View Source
var CapabilityLabelMap = map[string]string{

	"Box":          spec.LabelBox,
	"Version":      spec.LabelVersion,
	"Registry":     spec.LabelRegistry,
	"Bootc":        spec.LabelBootc,
	"Status":       spec.LabelStatus,
	"Info":         spec.LabelInfo,
	"CandyVersion": spec.LabelCandyVersion,

	"UID":  spec.LabelUID,
	"GID":  spec.LabelGID,
	"User": spec.LabelUser,
	"Home": spec.LabelHome,

	"Port":      spec.LabelPort,
	"PortProto": spec.LabelPortProto,
	"PortRelay": spec.LabelPortRelay,
	"Volume":    spec.LabelVolume,
	"Alias":     spec.LabelAlias,
	"Route":     spec.LabelRoute,

	"Security": spec.LabelSecurity,

	"Network": spec.LabelNetwork,

	"Env":        spec.LabelEnv,
	"EnvCandy":   spec.LabelEnvCandy,
	"PathAppend": spec.LabelPathAppend,

	"Init":         spec.LabelInit,
	"InitDef":      spec.LabelInitDef,
	"Service":      spec.LabelService,
	"ServiceNames": spec.LabelInit,

	"Distro":      spec.LabelPlatformDistro,
	"BuildFormat": spec.LabelPlatformFormat,
	"Builder":     spec.LabelBuilderUse,
	"Build":       spec.LabelBuilderProvide,

	"Hook": spec.LabelHook,

	"Skill": spec.LabelSkill,

	"DataEntries": spec.LabelDataEntries,
	"DataImage":   spec.LabelDataBox,

	"EnvProvide":       spec.LabelEnvProvide,
	"EnvRequire":       spec.LabelEnvRequire,
	"EnvAccept":        spec.LabelEnvAccept,
	"SecretAccept":     spec.LabelSecretAccept,
	"SecretRequire":    spec.LabelSecretRequire,
	"Secret":           spec.LabelSecret,
	"MCPProvide":       spec.LabelMCPProvide,
	"AgentProvide":     spec.LabelAgentProvide,
	"TerminalProfiles": spec.LabelTerminalProfiles,
	"MCPRequire":       spec.LabelMCPRequire,
	"MCPAccept":        spec.LabelMCPAccept,

	"Description": spec.LabelDescription,

	"Shell": spec.LabelShell,

	"CheckLevel": spec.LabelCheckLevel,
}

CapabilityLabelMap names every OCI label that participates in the capabilities contract. Maintained alongside spec.BoxMetadata — adding a field to spec.BoxMetadata without adding an entry here trips the completeness check below and breaks the build.

View Source
var DeployOnlyCapabilityFields = map[string]bool{
	"Tunnel":    true,
	"DNS":       true,
	"AcmeEmail": true,
	"Engine":    true,
}

DeployOnlyCapabilityFields are spec.BoxMetadata fields that are NOT baked as OCI labels by design — they're populated from charly.yml overlays (or deploy-host config) and have no image-declaration meaning. The completeness check exempts them from CapabilityLabelMap mapping.

This list codifies the schema v4 migration note on labels.go: "Tunnel / DNS / AcmeEmail / Engine moved to BundleNode". The fields stay on spec.BoxMetadata because deploy-mode commands still consume them after MergeDeployOntoMetadata runs — but they never round-trip through OCI labels.

View Source
var EncMountDeadline = 2 * time.Minute

EncMountDeadline bounds how long ResolveEncPassphraseForMount will retry transient failures (source="unavailable") before giving up. source="locked" does NOT use this — it uses event-driven DBus signal waiting with no deadline (see the caller-supplied waiter).

View Source
var EncMountPollPeriod = 5 * time.Second

EncMountPollPeriod is the interval between retry attempts for source="unavailable" only.

View Source
var FuseConfPath = "/etc/fuse.conf"

FuseConfPath is the fuse.conf location; a package var so tests point it elsewhere.

View Source
var InspectLabels = kit.InspectImageLabels

InspectLabels reads OCI labels from a local image via engine inspect. Package-level var for testability (mocked by callers that need to exercise ExtractMetadata's decode logic without a real container store).

View Source
var IsEncryptedMounted = DefaultIsEncryptedMounted

IsEncryptedMounted checks if the plain dir is a FUSE mount by reading /proc/mounts. Package-level var for testability.

View Source
var OpInContext func(op *Op, ctx spec.ExecContext) bool

OpInContext reports whether an op runs in the given exec context. Its fallback consults the kernel VerbCatalog (charly), so charly injects the impl at init. ExecContext (+ Ctx consts) is spec.ExecContext — a plain shared vocabulary type (K3, #39).

View Source
var PluginEmitStepWords = spec.PluginEmitStepWords
View Source
var RemoveEnvdFile = kit.RemoveEnvdFile

RemoveEnvdFile removes a candy's env.d entry. It is a plain forward to kit.RemoveEnvdFile (a pure filesystem helper with zero charly-core dependency — the earlier "injected seam, charly-core sets it via init()" pattern was residue from before the helper was proven portable; TeardownHostDeploy's best-effort call site discards the error either way).

View Source
var ShellAllowlist = map[string]bool{"bash": true, "zsh": true, "fish": true, "sh": true}

ShellAllowlist enumerates valid per-shell sub-block keys inside `shell:`.

View Source
var TaskAutoExports = map[string]bool{
	"USER":       true,
	"UID":        true,
	"GID":        true,
	"HOME":       true,
	"ARCH":       true,
	"BUILD_ARCH": true,
}

TaskAutoExports are the auto-exported variable names reserved for the generator; `vars:` entries may not shadow these, and every `${VAR}` reference resolves against (auto-exports ∪ candy.Vars).

View Source
var TaskVarRefPattern = regexp.MustCompile(`\$\{([A-Z_][A-Z0-9_]*)\}`)

TaskVarRefPattern matches ${NAME} references in task fields.

Functions

func AbsoluteCandySequence added in v0.2026193.1801

func AbsoluteCandySequence(boxName string, boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel, globalOrder []string) []string

AbsoluteCandySequence returns an image's complete candy set (own + entire base chain) as a subsequence of the global order.

func AcceptedEnvSet added in v0.2026198.358

func AcceptedEnvSet(accepts, requires []spec.EnvDependency) map[string]bool

AcceptedEnvSet builds a set of env var names from env_accepts and env_requires declarations. Used to filter which env_provides vars get injected into a consumer.

func AddTransitiveDeps added in v0.2026193.1801

func AddTransitiveDeps(candyName string, layers map[string]CandyModel, needed map[string]bool, excluded map[string]bool)

AddTransitiveDeps adds all transitive dependencies of a candy to the needed set.

func AppendOrReplaceEnv added in v0.2026190.848

func AppendOrReplaceEnv(envs []string, entry string) []string

AppendOrReplaceEnv adds or replaces an env var entry (KEY=VALUE) in a slice. If the key already exists, the value is replaced in-place.

func AppendShellPathLines added in v0.2026190.848

func AppendShellPathLines(body string, paths []string, shell, home string) string

AppendShellPathLines appends path_append entries to the snippet body using shell-appropriate syntax. bash/zsh/sh use POSIX `PATH=$PATH:X` exports; fish uses `fish_add_path X`. Idempotent: if body already ends with a newline, no extra blank line is inserted.

func ArgHasImageTag added in v0.2026190.848

func ArgHasImageTag(arg string) bool

ArgHasImageTag reports whether arg's trailing path segment carries a ":tag" — the marker of a registry IMAGE ref (ghcr.io/org/image:tag), as opposed to a github REPO ref (which pins with @version) or a dotted-path deploy address. Shared by CanonicalizeDeployArg + the deploy-name guard (R3).

func BakeableSteps added in v0.2026202.1522

func BakeableSteps(plan []spec.Step) []spec.Step

BakeableSteps returns the subset of a plan that belongs in the runtime descriptor label per the bake rule above.

func BareRef added in v0.2026190.848

func BareRef(ref string) string

BareRef returns the map-key form of a ref (no @ prefix, no :version).

func BedCheckLiveRefs added in v0.2026190.848

func BedCheckLiveRefs(name string, children map[string]*BundleNode) []string

SortedNestedKeys returns the keys of a children map in deterministic order so traversal produces stable output across runs. BedCheckLiveRefs returns the ordered `charly check live` targets for a bed: the substrate itself first, then each nested child as a sorted dotted path. This is the pure list `charly check run` walks so a nested pod's BAKED candy/box check (e.g. the selkies candy's encoder + frame checks on a nested selkies-kde pod) is exercised against its real venue through the chain — not just the parent substrate. Without the nested entries, `charly check run` deploys nested children but never evaluates them. Pure + unit-tested.

func BoxCandyChain added in v0.2026202.1522

func BoxCandyChain(cfg *spec.Config, layers map[string]CandyModel, boxName string) ([]string, error)

BoxCandyChain returns the ordered, de-duplicated candy map-keys for boxName across its FULL base-image chain (box → base → base's base), candy-order per level. This is the ONE walk every BASE-CHAIN field collector shares (CollectHooks, CollectShell, CollectDescriptions, CollectBoxVolume, CollectBoxPorts) — so a contribution a base box makes (a volume, a check check, a published port) is inherited by every box built on it. De-duplication is first-occurrence-wins by candy key.

On a ResolveCandyOrder error at a level the walk stops there, returning what was collected so far PLUS the error — callers that propagate it keep doing so; callers that swallow it and use the partial result keep doing that by ignoring the returned error.

func BoxDirectCandies added in v0.2026202.1522

func BoxDirectCandies(cfg *spec.Config, layers map[string]CandyModel, boxName string) ([]string, error)

BoxDirectCandies returns the ordered, transitively-resolved candy map-keys for boxName's OWN candies only — NO base-chain traversal. The shared walk for LEAF-SPECIFIC fields (CollectSecurity, CollectBoxAlias) that intentionally do NOT inherit from a base box.

func BoxDirectDeps added in v0.2026192.1709

func BoxDirectDeps(name string, img *buildkit.ResolvedBox, boxes map[string]*buildkit.ResolvedBox, includeFormatBuilders bool) []string

BoxDirectDeps returns the direct box-build dependencies of img:

  • Base (when not an external OCI ref)
  • Builder.AllBuilder() format-builder boxes (only when includeFormatBuilders)
  • BootstrapBuilderImage (the `from: builder:pacstrap` / debootstrap source)

Self-refs and refs to boxes not in the map (for builder + bootstrap builder) are filtered out. Base is appended only when it names an internal box; builder-backed images legitimately have no base.

One helper, three callers (ResolveBoxOrder, ResolveBoxLevels, FilterBox below) so adding a future edge kind lands in one place.

func BoxNeedsBuilder added in v0.2026192.1709

func BoxNeedsBuilder(img *buildkit.ResolvedBox, boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) bool

BoxNeedsBuilder returns true if any of the box's own resolved candies (excluding parent-provided) have pixi.toml, package.json, or Cargo.toml. When candies is nil, falls back to unconditional builder dependency.

func BuildArchExports added in v0.2026192.1709

func BuildArchExports() string

BuildArchExports emits the shell prelude that maps `uname -m` to a Go-style ARCH (amd64/arm64/arm) for download/build steps.

func BuildArtifactEnv added in v0.2026201.1934

func BuildArtifactEnv(secretEnv map[string]string, node *spec.BundleNode) map[string]string

BuildArtifactEnv composes the env used for candy-artifact path substitution: the resolved secret env first, then the deploy node's own env: lines overlaid (last-wins). nil node contributes nothing.

Shared by the local deploy target.Add / the vm deploy's Add path — both feed it to RetrieveCandyArtifacts so rewrite rules like ${K3S_KUBECONFIG_SERVER} resolve to the declared value rather than a literal placeholder. The node is the dispatch-merged BundleNode (never re-read from disk).

func BuildDepPkgsOnHost added in v0.2026198.914

func BuildDepPkgsOnHost(_ context.Context, lp *LocalPkgDef, bDef *BuilderDef, builderImage string, packages []string, candyDir string, resolveImage func(string) (string, error), ensureImage func(context.Context, string) error, opts EmitOpts) ([]string, error)

BuildDepPkgsOnHost builds an arbitrary set of dependency packages into package files ON THE HOST (where podman is available) through the EXISTING builder named by LocalPkgDef.DepBuilder (the `aur` builder for pac) and returns the produced package paths. It is the BUILD half of the VM target's aur `execBuilder` path factored out (R3): execBuilder now calls this and then TransferAndInstallPkgs, and the localpkg step calls it to build the package's dependency closure. There is exactly ONE host-side dep-builder implementation across the candy-aur path and the localpkg-dep-closure path.

It synthesizes a BuilderStep{Builder:lp.DepBuilder, …} carrying the package names in RawStageContext["packages"], renders the SAME renderBuilderScript the container/local/VM builder paths use, wraps it with the same root backstop-find + chown-to-0:0 (so the bind-mount surface is host-readable under rootless podman), runs it via BuilderRun(RunAsRoot:true), surfaces output to stderr, and globs the staging dir for LocalPkgDef.PkgGlob.

Empty packages → (nil, nil): a no-op, never an error. On DryRun it logs the plan and returns nil (no artifacts).

resolveImage/ensureImage are INJECTED: resolving a namespace-qualified / short builder ref to a concrete image, and auto-building it on demand via `charly box build`, needs the still-core loader (*Config + project dir) — a genuine, isolated host dependency this file cannot (and should not) absorb. The caller supplies the closures; a nil pair means "no resolve/ensure" (the caller already resolved builderImage, or accepts a bare literal).

The staging tmpdir is registered for sweep but deliberately NOT defer-removed: the caller owns the returned package files until install completes.

func BuildLocalPkgOnHost added in v0.2026198.914

func BuildLocalPkgOnHost(ctx context.Context, lp *LocalPkgDef, srcDir string, opts EmitOpts) ([]string, error)

BuildLocalPkgOnHost builds the package(s) defined by the source dir on the HOST by rendering LocalPkgDef.BuildTemplate and returns the produced package-file paths (globbed via LocalPkgDef.PkgGlob). Both the build working tree ({{.SrcDir}}) and output ({{.PkgDest}}) are per-call temporary directories, so package tools cannot rewrite tracked definitions or leave build trees in the authored source. {{.SourceDir}} names the original source only for templates that intentionally read project-relative inputs.

The build command (e.g. makepkg) comes ENTIRELY from config — this function renders LocalPkgDef.BuildTemplate via the existing RenderTemplate engine and runs it under `bash -c`, so there is no hardcoded makepkg/pacman literal here.

The temp dir is registered for sweep but deliberately NOT defer-removed: the caller owns the package files until install completes.

func BuildServiceRenderContext added in v0.2026198.254

func BuildServiceRenderContext(entry *spec.ServiceEntry, ctx spec.ServiceRenderContext) spec.ServiceRenderContext

BuildServiceRenderContext fills the entry-derived, home-expanded render context (a pure spec.ServiceEntry projection — no init-system knowledge). The plugin renders its templates against this; the packaged/drop-in branch decisions are precomputed here (PackagedUnit, RenderDropin) so the plugin renders from the ctx alone.

func BuildStartArgs added in v0.2026198.345

func BuildStartArgs(engine, imageRef string, uid, gid int, ports []string, name string, volumes []spec.VolumeMount, bindMounts []ResolvedBindMount, gpu bool, bindAddr string, envVars []string, security spec.SecurityConfig, entrypoint []string, workingDir string, network ...string) []string

BuildStartArgs constructs the container run argument list for a detached service. entrypoint is the init system command (e.g., ["supervisord", "-n", "-c", "/etc/supervisord.conf"]) or the fallback (e.g., ["sleep", "infinity"]).

func BuilderCtxKey added in v0.2026190.848

func BuilderCtxKey(candy, builder string) string

BuilderCtxKey keys the per-(candy,builder) pre-resolved builder context.

func BuilderResolveInputFrom added in v0.2026194.1727

func BuilderResolveInputFrom(candyName, builderName string, builderDef *buildkit.BuilderDef, ctx *spec.BuildStageContext) spec.BuilderResolveInput

BuilderResolveInputFrom builds the serializable spec.BuilderResolveInput a builder plugin's OpResolve leg needs, from the host-computed BuildStageContext. Cache mounts are PRE-RENDERED to flag strings here (buildkit.RenderCacheMounts) with the SAME separator/trailing the former cacheMountsOwned/cacheMountsAuto template funcs used, so the plugin's rendered stage is byte-identical to the former embedded-vocabulary render. Shared by the box-build path (resolveDetectionBuilderReply) AND the pod-overlay build-emit (charly stepEmitBuilder) + the inline-builder seam (charly resolveInlineBuilderSeam), R3 — hence exported.

func BuilderStepImage added in v0.2026209.2002

func BuilderStepImage(s *BuilderStep, opts EmitOpts) (string, error)

BuilderStepImage resolves the builder image ref for a BuilderStep: --builder-image override -> the compiled BuilderStep.BuilderImage. The builder always runs on the HOST (podman); the venue never needs a container runtime.

func BundleWalkPostOrder added in v0.2026190.848

func BundleWalkPostOrder(n *BundleNode, path string, fn func(path string, node *BundleNode) error) error

WalkPostOrder invokes fn on every child (recursively, post-order) before invoking fn on this node. Post-order is the delete-order semantic: a child must be torn down while its parent environment is still alive, so the caller reverses leaves first.

func BundleWalkPreOrder added in v0.2026190.848

func BundleWalkPreOrder(n *BundleNode, path string, fn func(path string, node *BundleNode) error) error

WalkPreOrder invokes fn on this node, then recurses into every child in sorted key order. Pre-order is the add-order semantic: a parent's environment must exist before its children can run inside it, so the caller applies deploys root-first.

fn receives the full dotted path to each node (e.g. "stack.web.db"). The root path argument is prepended; callers pass the node's own key as `path`.

When fn returns a non-nil error, traversal stops immediately and the error propagates.

func CandyArtifactRegisters added in v0.2026201.1934

func CandyArtifactRegisters(layers []spec.CandyReader) map[string]bool

CandyArtifactRegisters returns the DISTINCT `register:` hints declared across every candy's artifact list — name-blind (it reads each artifact's own declaration, never a candy name). The declaration-reading half of the k3s-server artifact-declaration-driven dispatch fix (P13-KERNEL); the WORD-KEYED HANDLER TABLE (function pointers into k3sPostProvision and friends) stays PLUGIN-side as the thin dispatch anchor (candy/plugin-bundle/secrets_artifacts.go's artifactRegisterHandlers, Cone A shape 3 — relocated wholesale from the deleted charly/deploy_add_shared.go), never here.

func CandyMapKey added in v0.2026192.1709

func CandyMapKey(layer CandyModel) string

CandyMapKey returns the candy's map key: the full @github ref for a remote candy (RepoPath/SubPathPrefix+Name), else the bare Name. Relocated from charly (P8).

func CandyNeedsBuilderStep added in v0.2026190.848

func CandyNeedsBuilderStep(layer CandyModel, bDef *BuilderDef) bool

CandyNeedsBuilderStep mirrors Generator.candyNeedsBuilder without requiring a Generator receiver — the compiler doesn't have one.

func CandyProvidedByBox added in v0.2026192.1709

func CandyProvidedByBox(boxName string, boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) (map[string]bool, error)

CandyProvidedByBox returns the set of candies installed by a box (including those inherited from parent boxes via base chain)

func CandyStageDirName added in v0.2026192.1709

func CandyStageDirName(layer CandyModel) string

CandyStageDirName returns the content-addressed staging dir name for a remote candy's copied tree (name.version). Relocated from charly (P8); byte-identical.

func CanonicalizeDeployArg added in v0.2026190.848

func CanonicalizeDeployArg(arg, instance string) (box, inst string)

CanonicalizeDeployArg splits Pattern A "<base>/<instance>" CLI positional args into their component (image, instance) pair. Idempotent: if the input is already split (instance != "") or contains no slash, returns as-is. Pattern B (FQ ref containing "/") is identified by presence of "@" or ":" suffix on the leftmost segment OR a registry-host pattern (contains "." before the first "/") and passed through untouched.

MUST be called at the top of every CLI verb that takes a positional deploy-arg (`charly config`, `charly start`, `charly stop`, `charly shell`, `charly logs`, `charly update`, `charly status`, `charly remove`) — before any downstream code reads c.Image or c.Instance. Without this, Pattern A instance deploys leak past the canonicalization boundary and downstream code conflates the deploy key with the image short-name (see Bug 2/3 RCA notes — MergeDeployOntoMetadata composes wrong key, port/env overlays drop).

func CapabilitiesFromLabels added in v0.2026202.1522

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

CapabilitiesFromLabels is the source-less loader used by `charly bundle from-box`: given only an engine + image ref, pull OCI labels via inspect and produce a spec.BoxMetadata struct. No charly.yml, no source repo access required. Errors propagate ErrImageNotLocal when appropriate (caller can wrap with a "run charly box pull" hint).

func CascadeTagChain added in v0.2026190.848

func CascadeTagChain(img *ResolvedBox) []string

ResolveCascadePackages is THE single distro-specificity cascade resolver, shared by EVERY package-emitting path — the deploy compiler (CompileSystemPackageSteps) AND the image-build Containerfile emitter (generate.go writeCandySteps). There is exactly one resolution so build and deploy can never diverge.

It computes the primary-format package set for a candy on an image: the candy's top-level `package:` BASE, UNION every matching distro tag section walked most-specific-first over img.Distro (deduped), with the Raw extras repo/copr/options/exclude/module resolved MOST-SPECIFIC-WINS. Returns the package list, the rendered Raw install context (including the unioned `package` list), and whether any tag section matched. CascadeTagChain returns the full per-candy cascade tag chain MOST-SPECIFIC FIRST: the image's distro chain (e.g. [debian:13, debian]) followed by the package-format FAMILY tag (img.Pkg = deb/pac/rpm) as the LEAST-specific level. A `distro: deb:` candy block therefore applies to EVERY deb-format distro (debian + ubuntu + their versions), `pac:` to arch + cachyos, `rpm:` to fedora — the family-generic level of the YAML-configured deb/pac/rpm → distro → version hierarchy. img.Pkg is the primary package format declared by the embedded vocabulary (charly/charly.yml), so the hierarchy lives entirely in YAML.

Distro INHERITANCE is the complementary YAML mechanism: img.Distro is already expanded (at resolve time, ExpandPackageInheritance) to include any `inherit_packages: true` ancestor, so a cachyos image/VM carries [cachyos, …, arch] and a `distro: arch:` block DOES reach cachyos — while ubuntu (no flag) stays isolated from debian. Both knobs live entirely in the embedded vocabulary (charly/charly.yml).

func CheckCapabilityLabelCompleteness added in v0.2026202.1522

func CheckCapabilityLabelCompleteness() error

CheckCapabilityLabelCompleteness returns an error listing any spec.BoxMetadata exported field that lacks an entry in CapabilityLabelMap. Called from TestCapabilityLabelCompleteness to fail the build when a field is added without a label mapping.

func CipherPopulatedPlainEmpty added in v0.2026202.105

func CipherPopulatedPlainEmpty(cipherDir, plainDir string) bool

CipherPopulatedPlainEmpty reports whether the gocryptfs cipher directory holds user data (anything beyond the gocryptfs.conf + gocryptfs.diriv metadata files) AND the plain mount target is empty. The combination means FUSE is unmounted on top of a populated vault — letting a container start now would silently bind the empty plain/ as a plaintext directory and write new data on top of the encrypted tree.

Returns false on stat errors (the surrounding error path will surface those — this helper is only a discrimination hint).

func ClassifyNodeTarget added in v0.2026205.1558

func ClassifyNodeTarget(node *spec.BundleNode, path string) string

ClassifyNodeTarget picks the target discriminator for a node. Uses node.Target when non-empty (canonical pod|vm|k8s|local|android, set from the node-form kind by the loader's bundleTargetForDisc).

For ref-based deploys with no charly.yml entry (e.g. `charly bundle add foo ./box.yml` where foo isn't declared), the deploy name itself is the hint: literal `host` or `local` (as a whole path LEAF) → local target; anything else → pod. A pure function of node+path with no LoadUnified/executor dependency — shared by charly core (deriving the ANCESTOR executor chain in a nested deploy walk) and candy/plugin-bundle (classifying the CURRENT node before dispatch, W4 pure-helpers relocation) — R3, one source for both call sites.

func ClassifyTarget added in v0.2026190.848

func ClassifyTarget(node *BundleNode) string

ClassifyTarget normalizes the Target field for dispatch. Empty Target falls back to "pod" (the default for named deploys); otherwise Target is the canonical source of truth (pod|vm|k8s|local|android — set from the node-form kind by bundleTargetForDisc; no name-prefix heuristic).

func CleanDeployEntry added in v0.2026196.928

func CleanDeployEntry(boxName, instance string, marshalNode func(name string, node *BundleNode) (*yaml.Node, error))

CleanDeployEntry removes an image's entry from charly.yml (best-effort). Also removes global service env vars injected by this image. If charly.yml becomes empty after removal, the file is deleted. Relocated from charly/deploy.go (K5-Unit-1); the flock is a kind-blind kit primitive (kit.AcquireFileLock) and the loader reaches core through the DeployStateHost seam. marshalNode is the deploy-kind-specific node-form serializer the caller supplies.

func CleanupBuiltPackageFiles added in v0.2026200.1312

func CleanupBuiltPackageFiles(pkgFiles []string) error

CleanupBuiltPackageFiles releases the temporary package directory returned by BuildLocalPkgOnHost or BuildDepPkgsOnHost after its final consumer has copied, transferred, or installed the artifacts. It refuses every path outside the two MkdirTemp namespaces under the process temp root, so a caller mistake can never broaden cleanup into an arbitrary directory.

func CollectAllBoxCandies added in v0.2026192.1709

func CollectAllBoxCandies(boxName string, boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) []string

CollectAllBoxCandies returns an image's complete candy set (its own resolved candies plus every candy inherited through the full base chain), in first-seen order. Relocated from charly core (P8); byte-identical to the former charly/intermediates.go collectAllBoxCandies.

walked is an IMAGE-visited guard for the base-chain recursion: a base edge may form a cycle (A.base=B, B.base=A) — caught + reported by ResolveBoxOrder, but without this guard the walk recurses a cyclic chain until the stack overflows. Re-visiting a base also can't add new candies, so skipping it is correct for the acyclic case too.

func CollectBoxAlias added in v0.2026209.2002

func CollectBoxAlias(cfg *spec.Config, layers map[string]spec.CandyReader, boxName string) ([]spec.CollectedAlias, error)

CollectBoxAlias gathers aliases from the box's own candies + box-level config. No base-chain traversal — aliases are leaf-box specific. Candy aliases come first; box-level entries override by name. Relocated from charly/alias_collect.go.

func CollectBoxPorts added in v0.2026209.2002

func CollectBoxPorts(cfg *spec.Config, layers map[string]CandyModel, boxName string) ([]string, error)

CollectBoxPorts returns an image's aggregated exposed container ports, walking the box's full candy chain and deduplicating by port number (with a "/proto" suffix when non-http).

func CollectCandySecretAccepts added in v0.2026202.1112

func CollectCandySecretAccepts(boxName, instance string, meta *spec.BoxMetadata, credServiceVNC string, cred CredentialAccess) (collected []CollectedSecret, resolutions []SecretResolution)

CollectCandySecretAccepts synthesizes CollectedSecret entries from an image's secret_accepts and secret_requires label metadata, resolving each against the credential store and returning:

  • []CollectedSecret: one entry per secret whose value was successfully resolved (non-empty). Entries carry Service/Key overrides from the candy manifest `key:` field (default: charly/secret/<env-var-name>) and RotateOnConfig=true so every charly config reconciles them with the latest credential store value (see plan §2.3).
  • []SecretResolution: one entry per input spec, reporting the source classification and whether the resolution succeeded. Required entries with Resolved=false are later caught by charly's checkMissingSecretRequires as a hard-fail condition.

This function does NOT touch the podman secret store — that's the job of ProvisionPodmanSecrets. It only reads from the credential store. No network calls, no filesystem mutations, safe to run speculatively.

func CollectDescriptions added in v0.2026202.1522

func CollectDescriptions(cfg *spec.Config, layers map[string]spec.CandyReader, boxName string) *spec.LabelDescriptionSet

CollectDescriptions returns nil if every section is empty.

func CollectShell added in v0.2026209.2002

func CollectShell(cfg *spec.Config, layers map[string]CandyModel, boxName string) *spec.LabelShellSet

shell_collect.go — the box SHELL-INIT aggregator (relocated from charly/shellcollect.go in the core-min wave-3 build-cluster split). CollectShell is a PURE candy-chain walk (spec.Config + layers, via BoxCandyChain) producing the three-section spec.LabelShellSet, beside its sibling chain-collectors (CollectDescriptions, MergeCandyHooks). Mirrors CollectDescriptions shape: dedupe by candy name, walk the composed candies, terminate on visited-image cycle.

Section assignment:

  • Each candy's `shell:` (intrinsic + per-shell sub-blocks) -> Candy.
  • Box-level `shell:` -> Box.
  • Deploy is never populated: the deploy-scope `shell:` overlay authoring field was retired outright; the LabelShellSet.Deploy wire section stays (a stable three-section label shape) but permanently empty until a properly-designed feature populates it.

Returns nil if every section is empty.

func CollectSubtreeBoxes added in v0.2026193.1801

func CollectSubtreeBoxes(node *TrieNode) []string

CollectSubtreeBoxes returns every terminal user image in the subtree rooted at node — the images terminating at node plus all images in descendant nodes. These are exactly the images that will base, directly or transitively, on an auto-intermediate created at this node, so they define the union of build formats / distro tags the intermediate must carry (see createIntermediate).

func ComputeDeployID added in v0.2026190.848

func ComputeDeployID(box string, layers, addCandies []string) string

ComputeDeployID returns a deterministic hex hash that identifies a specific deploy (image + ordered candy set + add_candy). Used as the ledger key so re-deploys of the same config are recognizable and candy-refcount bookkeeping is stable.

func ComputeEffectiveVersions added in v0.2026192.1709

func ComputeEffectiveVersions(boxes map[string]*buildkit.ResolvedBox, candies map[string]CandyModel) error

ComputeEffectiveVersions assigns ResolvedBox.EffectiveVersion for every image in the build graph. EffectiveVersion is the content-derived identity emitted as the ai.opencharly.version OCI label. Relocated from charly core (P8); byte-identical.

  1. the image's dedicated `version:` (img.Version) if set; else
  2. the highest candy `version:` across its full candy set (CollectAllBoxCandies spans the entire base chain); else
  3. the internal base image's EffectiveVersion (recurse); else
  4. a HARD ERROR — there is NO build-timestamp fallback.

Run AFTER ComputeIntermediates + GlobalCandyOrder so the base chain and the auto-intermediate images are fully materialized in boxes.

func ComputeIntermediates added in v0.2026198.914

func ComputeIntermediates(boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel, defaults IntermediateDefaults, tag string) (map[string]*buildkit.ResolvedBox, error)

ComputeIntermediates analyzes all images, groups them by (Base, UID), builds prefix tries of relative candy sequences within each sibling group, creates intermediates at branching points, and returns updated images map. User-defined images always take priority over auto-intermediates.

func ComputeOwnCandies added in v0.2026193.1801

func ComputeOwnCandies(parentName string, pathCandies []string, result map[string]*buildkit.ResolvedBox, layers map[string]CandyModel, globalOrder []string, pixiBound map[string]bool) []string

ComputeOwnCandies determines which candies an intermediate needs to install (pathCandies minus what the parent already provides).

func CredKeyForSecret added in v0.2026202.105

func CredKeyForSecret(boxName, instance string) string

CredKeyForSecret returns the credential key for an image/instance pair.

func CredServiceForSecret added in v0.2026202.105

func CredServiceForSecret(envVar, credServiceVNC string) string

CredServiceForSecret maps well-known env vars to credential services.

func DefaultAskPassword added in v0.2026202.105

func DefaultAskPassword(id, prompt string) (string, error)

func DefaultIsEncryptedMounted added in v0.2026202.105

func DefaultIsEncryptedMounted(plainDir string) bool

func DeployNestedLocalChildren added in v0.2026202.1112

func DeployNestedLocalChildren(parent string, children map[string]*BundleNode, apply func(childKey, dotted string) error) error

DeployNestedLocalChildren deploys a VM's nested target:local children via the dotted-path dispatch, which applies each child's local-deploy candies INSIDE the guest over the NestedExecutor (SSH).

plugin-deploy-vm's PostApply brings up nested target:pod children as in-guest quadlets, but it SKIPS target:local children — they carry no image, they apply candies in place. Without this loop a nested local child never deploys, and a deploy-scope check against it either fails or (worse) silently checks nothing.

Both sites that own a VM venue call this: the isVM bed ROOT and bringUpMembers' VM-member branch. They differ only in how a child deploy is executed (the root wraps it in a recorded step(); a member shells out directly), so that is the injected apply func.

func DeployStorageDir added in v0.2026193.1222

func DeployStorageDir(deployKey, instance string) string

DeployStorageDir is the per-deploy directory component for bind-auto paths and encrypted-volume directories. Like DeployVolumePrefix it is unique per deploy (base vs instance vs Pattern-B vs bed). For a base deploy with no instance it is just the deploy key; an instance appends "-<instance>".

func DeployVolumePrefix added in v0.2026193.1222

func DeployVolumePrefix(deployKey, instance string) string

DeployVolumePrefix is the named-volume prefix for a deploy: the deploy's container name plus a dash, so EVERY distinctly-named deploy (base, Pattern-B, instance, or kind:check bed) gets its own volume namespace. Two deploys never share a named volume unless they share a container name (which they can't). Single source of truth for volume naming — ResolveVolumeBacking, removeVolumes, and scopeVolumesToDeployKey all key off it.

func DescribePlan added in v0.2026190.848

func DescribePlan(p *InstallPlan) string

DescribePlan returns a short human-readable summary of a plan. Used by --dry-run table output and by TestDescribePlan for golden comparison.

func DescriptionInfo added in v0.2026196.928

func DescriptionInfo(d string) string

DescriptionInfo returns the human-facing summary: the FIRST line of the plain-string description (multi-line prose lives in the rest of the string). Relocated from charly/generate.go so MergeDeployOntoMetadata (deploy_file.go) reads it without a core dep.

func DetectExternalizedBuilders added in v0.2026194.1727

func DetectExternalizedBuilders(order []string, candies map[string]CandyModel, externalized map[string]bool, img *buildkit.ResolvedBox) []string

DetectExternalizedBuilders returns the externalized builder words an image triggers, so a caller can connect exactly those plugins on-demand. A config-only detection (aur) runs a distro-specific install_template, so it is only needed when the image's build formats include that format (the DetectConfig gate — a multi-distro candy's aur: section must NOT pull arch tooling onto a fedora build); a detect-files builder is needed by any candy carrying the file. Uses the img-less CandyNeedsBuilderStep (the DetectConfig BuildFormats gate is applied here, not in the step check). The SINGLE detection impl shared (R3) by the box-build RENDER (EmitBuilderStages, over the Generator's Candies + ExternalizedBuilders) AND the charly deploy build PRE-PASS (over its candy map + externalizedBuilders registry set).

func EmitBakedPlugins added in v0.2026209.2002

func EmitBakedPlugins(ctx context.Context, b *strings.Builder, buildDir, boxName string, candyOrder []string, candies map[string]CandyModel) error

EmitBakedPlugins bakes each composing candy's `bake_plugin:` out-of-tree plugin binaries into the FINAL image, staging under .build/<boxName>/.plugins/ and writing the COPY + chmod Containerfile fragment into b. Byte-identical logic to the former charly/generate.go (*Generator).emitBakedPlugins, adapted to run over (candies, buildDir) directly — no Generator, no host round-trip.

func EmitCmd added in v0.2026192.1709

func EmitCmd(b *strings.Builder, t vmshared.Op, layerStage string, img *buildkit.ResolvedBox, userIsRoot bool)

EmitCmd emits a single RUN for a cmd task, with the layer-stage /ctx bind mount plus cache mounts appropriate to the user (distro format caches for root, npm cache for non-root). Shell is bash via a single-quoted heredoc so authors' $(cmd) / ${VAR} stay intact for bash; BUILD_ARCH is injected as a shell var.

func EmitCopy added in v0.2026192.1709

func EmitCopy(b *strings.Builder, t vmshared.Op, layerStage string, img *buildkit.ResolvedBox)

EmitCopy emits a COPY --from=<layer-stage> for an existing file in the candy dir.

func EmitDownload added in v0.2026192.1709

func EmitDownload(b *strings.Builder, t vmshared.Op, img *buildkit.ResolvedBox) error

EmitDownload emits one RUN per download task: fetch to a content-addressed /tmp/downloads cache, then extract. Honors candy-declared `cache:` mounts.

func EmitLinkBatch added in v0.2026192.1709

func EmitLinkBatch(b *strings.Builder, tasks []vmshared.Op, img *buildkit.ResolvedBox)

EmitLinkBatch emits a single RUN with chained ln -sf for a batch of adjacent same-user link tasks.

func EmitMkdirBatch added in v0.2026192.1709

func EmitMkdirBatch(b *strings.Builder, tasks []vmshared.Op, img *buildkit.ResolvedBox)

EmitMkdirBatch emits a single RUN mkdir -p for a batch of adjacent same-user mkdir tasks, splitting per-mode chmod at the tail when modes differ.

func EmitSetcapBatch added in v0.2026192.1709

func EmitSetcapBatch(b *strings.Builder, tasks []vmshared.Op, img *buildkit.ResolvedBox)

EmitSetcapBatch emits a single RUN setcap … for a batch of adjacent setcap tasks (strip via -r for empty caps, set otherwise), chained with &&.

func EmitVarsEnv added in v0.2026192.1709

func EmitVarsEnv(b *strings.Builder, vars map[string]string)

EmitVarsEnv writes ENV directives for candy.Vars and the build-arg-sourced ARCH auto-export. Sorts vars by key for deterministic output; emitted once per candy.

func EmitWrite added in v0.2026192.1709

func EmitWrite(b *strings.Builder, t vmshared.Op, srcPath string, img *buildkit.ResolvedBox)

EmitWrite emits a COPY from the staged inline-content dir to the destination.

func EncNotStoredError added in v0.2026202.1112

func EncNotStoredError(boxName, backend, src string) error

EncNotStoredError formats the terminal "credential not stored" error with actionable remediation hints.

func EncPlanFor added in v0.2026202.105

func EncPlanFor(boxName, instance, volume, scopeDir string) ([]spec.EncVolumePlan, error)

EncPlanFor host-prelifts the per-volume gocryptfs execution plan for the given box/instance, filtered to `volume` when non-empty. It loads the deploy config (LoadEncryptedVolume), resolves each volume's cipher/plain dirs (ResolveEncVolumeDir), and probes the initialized/mounted state (IsEncryptedInitialized/IsEncryptedMounted). scopeDir is the scope-unit directory component: DeployStorageDir(box,instance) for mount/unmount/passwd, or the bare box name for the ensure path (a pre-existing derivation difference, identical for the common empty-instance case, preserved exactly by this cutover). The result is the self-contained plan candy/plugin-enc executes over OpExecute.

func EncPlanForConfig added in v0.2026206.837

func EncPlanForConfig(dc *BundleConfig, boxName, instance, volume, scopeDir string) ([]spec.EncVolumePlan, error)

EncPlanForConfig is EncPlanFor's SEAM-ROUTABLE sibling: identical plan-building logic, but takes an ALREADY-LOADED dc instead of reaching the package-level LoadBundleConfig() itself. EncPlanFor (and the LoadEncryptedVolume it calls) is placement-DEPENDENT — it silently degrades to "no encrypted volumes" (not an error) unless DeployStateHost is registered, which only happens in charly-core's own init(); safe today because candy/plugin-pod's command:config is compiled-in-only, but that is a per-BUILD fact, not a guarantee (the exact class of latent bug candy/plugin-pod/remove_orchestration.go's resolveSidecarNames hit and fixed the same way). A caller that might ever run out-of-process loads dc itself via LoadBundleConfigViaSeam (the EXISTING "pod-config-load-bundle" seam — R3, no new seam invented) and calls this instead — see candy/plugin-pod/enc_cmd.go.

func EncServiceFilename added in v0.2026202.105

func EncServiceFilename(boxName string) string

EncServiceFilename returns the systemd service filename for a legacy crypto companion unit. Used only for cleanup of stale enc services from older charly versions.

func EncStatus added in v0.2026202.105

func EncStatus(boxName, instance string) error

EncStatus prints the status of encrypted bind mounts for an image.

func EncStatusFromConfig added in v0.2026206.837

func EncStatusFromConfig(dc *BundleConfig, boxName, instance string) error

EncStatusFromConfig is EncStatus's SEAM-ROUTABLE sibling — identical output given an ALREADY-LOADED dc. See EncPlanForConfig's doc comment for the placement-dependency rationale a caller of this variant is avoiding.

func EncryptedCipherDir added in v0.2026194.615

func EncryptedCipherDir(storagePath, boxName, name string) string

EncryptedCipherDir returns the cipher (encrypted-blob) directory for an encrypted bind mount: <storagePath>/<EncryptedVolumeName>/cipher.

func EncryptedPlainDir added in v0.2026194.615

func EncryptedPlainDir(storagePath, boxName, name string) string

EncryptedPlainDir returns the plain (FUSE mount-point) directory for an encrypted bind mount: <storagePath>/<EncryptedVolumeName>/plain.

func EncryptedVolumeName added in v0.2026194.615

func EncryptedVolumeName(boxName, name string) string

EncryptedVolumeName returns the directory name for an encrypted volume: charly-<box>-<name>.

func EnsureCandySecret added in v0.2026209.2002

func EnsureCandySecret(dep spec.EnvDependency, required bool, cred CredentialAccess) (val, source string)

EnsureCandySecret resolves a secret_requires/secret_accepts EnvDependency against the credential store (via cred). For required deps that miss everywhere (env, store), generates a 32-byte hex token, persists via cred.Write, and returns the new value. For optional deps that miss, returns "" with the source classification from cred.Resolve so the caller can fall back to dep.Default if set.

The Key field on an EnvDependency follows the format "<service>/<key>" and must start with "charly/" (enforced at load time by charly's validate.go). When Key is empty, the default lookup is service="charly/secret", key=Name.

Race-free across multiple candies declaring the same secret: the first caller's cred.Write lands in the active backend; the second caller's cred.Resolve reads the persisted value.

func EnsurePodmanSecret added in v0.2026202.105

func EnsurePodmanSecret(engine, name, value string) error

EnsurePodmanSecret creates or replaces a podman secret.

func EnsureServiceSuffix added in v0.2026190.848

func EnsureServiceSuffix(unit string) string

EnsureServiceSuffix adds `.service` if missing. Distinguishes unit types: if the caller already included a suffix (e.g. "foo.timer", "foo.socket"), leave it alone.

func EnvKey added in v0.2026190.848

func EnvKey(entry string) string

EnvKey extracts the KEY part from a KEY=VALUE string.

func EnvVarNameToPodmanSecretSlug added in v0.2026202.1112

func EnvVarNameToPodmanSecretSlug(envVarName string) string

EnvVarNameToPodmanSecretSlug converts an env var name to the slug used in the podman secret name (lowercase + underscores → hyphens). Relocated from charly/validate.go (Cutover B unit 6b) — its sole remaining caller was CollectCandySecretAccepts below.

func ExecLocalPkgInstall added in v0.2026198.914

func ExecLocalPkgInstall(ctx context.Context, exec DeployExecutor, s *LocalPkgInstallStep, supported bool, venueName string, opts EmitOpts) error

ExecLocalPkgInstall is the shared body both the local deploy target and the external vm deploy call for a LocalPkgInstallStep: resolve the package source dir, build it on the host, then transfer+install onto the target venue. `supported` gates whether the install leg runs (the venue's package manager must match the step's format); an unsupported target or a missing source dir is a clean no-op (the candy's own curl/COPY task covers it).

venueName is used only for log lines (e.g. "host", "vm:cachyos-gpu").

func ExpandCandy added in v0.2026192.1709

func ExpandCandy(requested []string, layers map[string]CandyModel) ([]string, error)

ExpandCandy expands candy composition references (candy: field in the candy manifest). For each candy that has IncludedCandies, recursively inserts them into the result. Candies without content (no install files, no env/ports/etc.) are omitted. Returns a flat, deduplicated candy list.

func ExtractMetadata added in v0.2026201.1934

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

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

func ExtractStringSlice added in v0.2026190.848

func ExtractStringSlice(m map[string]any, key string) []string

ExtractStringSlice returns m[key] as []string or nil if absent. Accepts []string and []interface{} (as produced by yaml.v3) inputs.

func FillBoxPlans added in v0.2026209.2002

func FillBoxPlans(cfg *spec.Config, layers map[string]spec.CandyReader, prefix string, out map[string][]spec.Step, visited map[*spec.Config]bool)

FillBoxPlans populates out with the include-ready FLATTENED acceptance plan for every box reachable from cfg (its own boxes + every import namespace, recursively), keyed by QUALIFIED name (`fedora.jupyter`). It mirrors the former in-core `include: box:<name>` arm EXACTLY: the SAME CollectDescriptions base-chain walk (candy-chain bakeable steps + the box-level bakeable plan) flattened over the three sections, so the relocated plugin box arm reads a byte-equivalent plan without the resolve engine. Only boxes with a non-empty plan are recorded. The visited set guards the pointer-keyed namespace cache against a self-referential cycle.

func FilterBox added in v0.2026206.837

func FilterBox(order []string, requested []string, boxes map[string]*buildkit.ResolvedBox) ([]string, error)

FilterBox filters the build order to only include the requested images and their dependencies (relocated from charly/build.go — the BUILD-cone cutover; pure over the resolved box graph, no loader/registry coupling). Collects requested images and their transitive deps (Base + format builders + BootstrapBuilderImage) via BoxDirectDeps, so this walker stays in lockstep with ResolveBoxOrder + ResolveBoxLevels (2026-05 cachyos-pacstrap-builder regression). includeFormatBuilders=true unconditionally: a filtered build set must always include format-builder images the requested targets need at build time, regardless of BoxNeedsBuilder.

func FilterOwnProvides added in v0.2026198.358

func FilterOwnProvides[T Named](entries []T, boxName string) []T

FilterOwnProvides removes entries injected by the given image (self-exclusion). Kept for RemoveBySource and other callers that need strict exclusion.

func FilterPreemptibleHolders added in v0.2026204.1329

func FilterPreemptibleHolders(tree map[string]spec.BundleNode) []spec.HolderDescriptor

FilterPreemptibleHolders returns every node in tree that declares itself a preemption holder (IsPreemptible), projected into spec.HolderDescriptor — the candidate set the arbiter may stop. Deterministic (sorted by name).

func FindPodSidecarQuadlets added in v0.2026198.358

func FindPodSidecarQuadlets(qdir, podName, mainContainerFile string) ([]string, error)

FindPodSidecarQuadlets returns the .container quadlets in qdir that belong to the pod podName, identified by the load-bearing `Pod=<podName>.pod` directive inside the quadlet's [Container] section. Filename-prefix matching is NOT used because it collides with sibling instances of the same image (e.g. charly-versa-ecovoyage.container is an instance of versa, NOT a sidecar of pod charly-versa.pod). Only true pod members carry the Pod= directive — sibling instances and standalone container deploys do not. mainContainerFile (typically the main pod container's quadlet filename) is excluded from the returned list because its lifecycle is owned by the caller's main systemctl disable, not by the sidecar sweep.

func FindVMClaimant added in v0.2026204.1329

func FindVMClaimant(tree map[string]spec.BundleNode, vmEntity string) (string, spec.BundleNode, bool)

FindVMClaimant finds a deploy/check node in tree that targets the given kind:vm entity and declares requires_exclusive — the claimant a standalone `charly vm create/stop/destroy <entity>` acquires/releases an exclusive lease for. ok=false when none exists. Ported verbatim from charly's former core-only lookupVMClaimant (first match wins over an unordered map iteration, same as before — a project is expected to declare at most one VM claimant per entity).

func FlattenedEnvMap added in v0.2026198.254

func FlattenedEnvMap(base map[string]string, overrides *spec.CandyServiceOverrides) map[string]string

FlattenedEnvMap merges an entry's base env with its overrides' env (overrides win).

func FormatCPUQuota added in v0.2026194.615

func FormatCPUQuota(cpus string) string

FormatCPUQuota converts a podman --cpus value ("2.5") to systemd's CPUQuota percentage form ("250%"); empty/unparseable → "" (skip the directive).

func FuseAllowOtherEnabled added in v0.2026202.105

func FuseAllowOtherEnabled() bool

FuseAllowOtherEnabled reports whether fuse.conf has an ACTIVE (uncommented, value-less) `user_allow_other` line. Every charly encrypted-volume mount uses `gocryptfs -allow_other` (so rootless podman keep-id can reach the FUSE plain dir), and fusermount3 REFUSES -allow_other unless this option is set. An absent/unreadable file counts as not enabled.

func GenerateAndStoreSecret added in v0.2026202.1112

func GenerateAndStoreSecret(service, key string, cred CredentialAccess) (val, source string)

GenerateAndStoreSecret generates a 32-byte url-safe base64 token (44 chars; Fernet-key-compatible — see GenerateRandomSecretToken), persists it to the active credential store at (service, key) via cred.Write, and returns the value with the "auto-generated" source classification. Persistence failures are logged to stderr but not returned as errors — the in-memory value is still usable for the current invocation.

Used by:

  • ProvisionPodmanSecrets — config-time CollectedSecret provisioning when --password=auto is in effect.
  • charly-core's ensureCandySecret (layer_secrets.go) — deploy-time secret_requires resolution on host/VM/SSH targets when the value is missing (calls this directly, supplying its own CredentialAccess).

func GeneratePodQuadlet added in v0.2026194.615

func GeneratePodQuadlet(cfg QuadletConfig) string

GeneratePodQuadlet produces the contents of a quadlet .pod file. The pod owns the shared network namespace: ports, network, and Tailscale tun interface.

Dual networking: the pod stays on the "charly" bridge (reachable by other containers) while the Tailscale sidecar adds a tun interface for exit node routing. --exit-node-allow-lan-access exempts bridge subnets from the tunnel.

func GenerateQuadlet added in v0.2026194.615

func GenerateQuadlet(cfg QuadletConfig) string

GenerateQuadlet produces the contents of a quadlet .container file. Pure.

func GenerateRandomSecretToken added in v0.2026202.105

func GenerateRandomSecretToken(byteCount int) string

GenerateRandomSecretToken returns `byteCount` random bytes encoded as url-safe base64 (RFC 4648 §5). For byteCount=32 this produces a 44-char string (43 base64 chars + 1 `=` pad).

Url-safe base64 was chosen over hex because it is a strict superset of what every secret consumer in the codebase needs:

  • Postgres / VNC / generic passwords: accept any string. Base64 has more entropy per character (6 bits vs 4) so 32 random bytes pack into 44 chars instead of hex's 64 chars.
  • Apache Airflow's AIRFLOW__CORE__FERNET_KEY: REQUIRES url-safe base64 of exactly 32 bytes (cryptography.fernet.Fernet's documented format). Hex strings are rejected with `binascii.Error: Invalid base64-encoded string`.
  • gocryptfs / Podman / KeePassXC: accept any string; format-agnostic.

Url-safe (vs standard base64) avoids `+` and `/` characters that would need shell-escaping in `[Service] Environment=...` quadlet lines and in `--password` CLI args. The `=` padding is benign in every consumer.

func GenerateSidecarQuadlet added in v0.2026194.615

func GenerateSidecarQuadlet(sc ResolvedSidecar, podName string) string

GenerateSidecarQuadlet produces the contents of a sidecar .container file.

func GenerateTunnelUnit added in v0.2026194.615

func GenerateTunnelUnit(cfg QuadletConfig, cfgPath string) string

GenerateTunnelUnit produces the companion systemd .service unit for a cloudflare tunnel. Relocated from charly core with the P11 config-write move (the deploy:pod plugin writes it); cfgPath (the cloudflared config path) is passed in by the host — the resolution helper tunnelConfigPath stays core, so this generator is pure. Returns "" for a non-cloudflare (or nil) tunnel. Byte-identical to the former core generateTunnelUnit given the same cfgPath.

func GlobalCandyOrder added in v0.2026193.1801

func GlobalCandyOrder(boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) ([]string, error)

GlobalCandyOrder computes a global topological order of all candies across all enabled images, using popularity (number of images needing each candy) as the primary tie-breaker and lexicographic as secondary.

func GpuVendorTokens added in v0.2026204.1329

func GpuVendorTokens(resources map[string]*spec.ResolvedResource) map[string]string

GpuVendorTokens projects a resolved resource map (spec.ResolvedProject.Resources, or the legacy host-side resource decode) to gpu-backed tokens -> PCI vendor — the only shape the arbiter's applyMode/firstPoisonedToken need. An arbitration-only token (no gpu: selector) is omitted, mirroring the former charly/arbiter_host.go resources() projection exactly.

func GraphReaches added in v0.2026193.1801

func GraphReaches(graph map[string][]string, from, to string) bool

GraphReaches reports whether `to` is reachable from `from` by following dependency edges (graph[x] lists the candies x depends on). Used to keep authored-list-order edge insertion cycle-safe in GlobalCandyOrder.

func HasEncryptedBindMounts added in v0.2026202.105

func HasEncryptedBindMounts(mounts []ResolvedBindMount) bool

HasEncryptedBindMounts returns true if any bind mount is encrypted.

func HasTailscaleSidecar added in v0.2026198.358

func HasTailscaleSidecar(sidecars map[string]json.RawMessage) bool

HasTailscaleSidecar reports whether a name-keyed sidecar map (opaque bodies) attaches the tailscale sidecar — a pure key-existence check.

func HolderAddrFor added in v0.2026204.1329

func HolderAddrFor(name string, node spec.BundleNode) spec.HolderAddr

HolderAddrFor computes the self-contained deployment address for a preemption holder/claimant node. It reads the node's own stamped Descent (set once, at LoadUnified/materialize time, by the host's stampBundleDescents) rather than performing a fresh registry lookup — so it is a pure function of already-materialized data, and works identically whether called against a freshly-loaded deploy tree (host-side) or a resolved-project envelope's Deploy map (plugin-side, Descent carried verbatim in the projection — never re-derived).

func InitHasRelayTemplate added in v0.2026192.1709

func InitHasRelayTemplate(def *spec.ResolvedInit) bool

InitHasRelayTemplate reports whether this init definition has a relay template. Relocated from charly (P8).

func InitRenderAssemblyTemplate added in v0.2026192.1709

func InitRenderAssemblyTemplate(def *spec.ResolvedInit) (string, error)

InitRenderAssemblyTemplate renders an init system's assembly_template (the step that assembles contributed unit fragments). Relocated from charly (P8).

func InitRenderPostAssemblyTemplate added in v0.2026192.1709

func InitRenderPostAssemblyTemplate(def *spec.ResolvedInit) (string, error)

InitRenderPostAssemblyTemplate renders the post_assembly_template (e.g. bootc container lint). Relocated from charly (P8).

func InitRenderRelayTemplate added in v0.2026192.1709

func InitRenderRelayTemplate(def *spec.ResolvedInit, port int, candyName string, index int) (string, error)

InitRenderRelayTemplate renders the relay_template for a port relay. Relocated from charly (P8).

func InitRenderStageFragmentCopy added in v0.2026192.1709

func InitRenderStageFragmentCopy(def *spec.ResolvedInit, boxName, fileName string) (string, error)

InitRenderStageFragmentCopy renders the stage_fragment_copy template. Relocated from charly (P8).

func InitRenderSystemEnableTemplate added in v0.2026192.1709

func InitRenderSystemEnableTemplate(def *spec.ResolvedInit, units []string) (string, error)

InitRenderSystemEnableTemplate renders the system_enable_template for the given distro-shipped units. Relocated from charly (P8).

func InjectSecretsIntoPlans added in v0.2026201.1934

func InjectSecretsIntoPlans(plans []*InstallPlan, env map[string]string)

InjectSecretsIntoPlans merges the resolved secret env map into every OpStep's task.Env across the supplied plans. Existing task.Env keys are preserved (candy-declared env takes precedence over a credential- store collision — a deliberate choice so an author can explicitly pin a value they control). Called from the deploy-add path after ResolveCandySecret and before target.Emit so the heredoc renderer sees the values as regular env exports.

func IntersectPlatforms added in v0.2026193.1801

func IntersectPlatforms(parent, defaults []string) []string

IntersectPlatforms returns platforms present in both slices. If the intersection is empty, returns parent (the more restrictive set).

func IpcModeBlocksShmSize added in v0.2026194.615

func IpcModeBlocksShmSize(ipcMode string) bool

IpcModeBlocksShmSize reports whether an IPC mode makes podman reject ShmSize.

func IsAutoVmDeployEntry added in v0.2026190.848

func IsAutoVmDeployEntry(entry BundleNode) bool

IsAutoVmDeployEntry reports whether a VM deploy entry carries NOTHING beyond the fields SaveVmDeployState auto-sets — target: vm, vm:, and vm_state. Such an entry is a pure runtime-state record (e.g. a disposable check-bed VM) that `charly vm destroy` should delete so it doesn't accumulate. Any OTHER non-zero field means operator-authored per-host config (preemptible, env, tunnel, port, security, …) that MUST survive a destroy→create cycle. Compares against the zero node after blanking the three auto-set fields, so a newly-added per-host field is covered automatically (no remembered append — same drift-proof discipline as MergeBundleNode).

func IsEncryptedInitialized added in v0.2026202.105

func IsEncryptedInitialized(cipherDir string) bool

IsEncryptedInitialized checks if gocryptfs has been initialized (gocryptfs.conf exists).

func IsRemoteCandyRef added in v0.2026190.848

func IsRemoteCandyRef(ref string) bool

IsRemoteCandyRef reports whether a ref is an @-prefixed remote ref.

func IsSameBaseBox added in v0.2026196.928

func IsSameBaseBox(source, boxName string) bool

IsSameBaseBox returns true if source is the same base image (with or without instance). Relocated from charly/deploy.go (used by RemoveBySource).

func IsTCPFamily added in v0.2026194.615

func IsTCPFamily(scheme string) bool

IsTCPFamily reports whether a scheme uses TCP-style forwarding.

func ListProvisionedSecretNames added in v0.2026202.105

func ListProvisionedSecretNames(engineBin, boxName string) []string

ListProvisionedSecretNames returns the engine-side podman secrets provisioned for a box (the charly-<box>-* names, sidecar secrets included), sorted — the charly-native replacement for ad-hoc `podman secret ls` verification (surfaced on `charly status <box>` detail).

func LoadEncryptedVolume added in v0.2026202.105

func LoadEncryptedVolume(boxName, instance string) ([]vmshared.DeployVolumeConfig, string, error)

LoadEncryptedVolume loads encrypted volume configs from charly.yml for an image. Returns the deploy volume configs with type=encrypted and the encrypted storage path.

func LoadEncryptedVolumeFromConfig added in v0.2026206.837

func LoadEncryptedVolumeFromConfig(dc *BundleConfig, boxName, instance string) ([]vmshared.DeployVolumeConfig, string, error)

LoadEncryptedVolumeFromConfig is LoadEncryptedVolume's SEAM-ROUTABLE sibling — identical lookup logic given an ALREADY-LOADED dc (nil dc == no per-host overlay, matching LoadBundleConfig's own nil-on-absent contract). See EncPlanForConfig's doc comment for the placement-dependency rationale a caller of this variant is avoiding.

func LocalizePort added in v0.2026194.615

func LocalizePort(mapping string, bindAddr string) string

LocalizePort applies a host bind address to a port mapping.

func MapToKeyValueSlice added in v0.2026192.1709

func MapToKeyValueSlice(m map[string]string) []spec.KeyValue

MapToKeyValueSlice deterministically sorts a map into []spec.KeyValue for template iteration. Matches the ServiceRenderContext contract. Relocated (P8).

func MarshalBundleNode added in v0.2026198.345

func MarshalBundleNode(node *spec.Deploy, primaries map[string]string) (*yaml.Node, error)

MarshalBundleNode emits a BundleNode as the compact name-first node-form the per-host overlay loader accepts: the kind discriminator carries the FULL body inline (scalars, collections, the resugared plan: step list), and nested/peer members become child nodes (recursive; discriminator inferred by bundleDiscForEntity). The loader-derived target / descent are dropped (target → the discriminator; descent → never persisted, a stored descent trips #DeployValue's descent?: _|_ on reload). Comment-preserving (yaml.v3 node API).

func MergeDeployOntoMetadata added in v0.2026196.928

func MergeDeployOntoMetadata(meta *spec.BoxMetadata, dc *BundleConfig, deployName, instance string)

MergeDeployOntoMetadata applies a per-host charly.yml entry's overrides (ports, env, security, tunnel, secrets, …) onto label-derived image metadata. Field-level replace semantics. Relocated from charly/deploy.go (K5-Unit-1).

The overlay entry is keyed by deployName — the charly.yml key base the caller is operating on (the bed / instance / Pattern-B name), NOT meta.Image (the baked ai.opencharly.box short-name). For a plain deploy the two coincide, but a kind:check bed or a Pattern-B deploy carries a key distinct from its image, so the caller MUST pass its own deploy key (typically c.Image). Keying off meta.Image would read whichever sibling deploy merely shares the image and clobber this entry's explicit port:/env:/security:.

func MergeEnvVars added in v0.2026190.848

func MergeEnvVars(existing, newVars map[string]string) map[string]string

MergeEnvVars merges new env vars into existing ones (upsert by key). New vars override existing vars with the same key.

func MergedDeployTree added in v0.2026204.1329

func MergedDeployTree(project map[string]spec.BundleNode, context string) map[string]spec.BundleNode

MergedDeployTree merges an already-loaded project deploy tree with the per-host deploy-config overlay (~/.config/charly/charly.yml) — exactly the merge the resource arbiter's holder/claimant gather has always performed (former charly/preempt.go gatherDeployNodes): per-host entries win per-field via MergeBundleNode. project may be nil/empty (no project loaded, e.g. a project-less `charly vm` invocation); the per-host overlay always loads independently. context is a short label threaded to LoadDeployConfigForRead's stderr warning so the caller is identifiable.

func NewEphemeralID added in v0.2026201.1934

func NewEphemeralID() (string, error)

NewEphemeralID returns six characters of cryptographically-strong random hex. Six characters is 24 bits of entropy — enough to make concurrent collisions vanishingly rare for a per-deploy lifecycle.

func NewSpecResolvedBox added in v0.2026195.1912

func NewSpecResolvedBox(v spec.ResolvedBoxView, distro map[string]*spec.ResolvedDistro, builder map[string]*spec.Builder) *buildkit.ResolvedBox

NewSpecResolvedBox rebuilds a *buildkit.ResolvedBox from its resolved-project envelope view + the project vocab maps (ResolvedProject.Distro / .Builder). It mirrors the host projectResolvedBox projection IN REVERSE (scalars) and re-attaches exactly the vocab pointers the deploykit render reads: DistroConfig / DistroDef / BuilderConfig (~35 img.DistroDef/DistroConfig/BuilderConfig read-sites). InitSystem/InitDef/CandyCaps are NOT re-attached — the render threads init via the activeInits param and reads no img.CandyCaps (build-mode caps are a host bootstrap concern, not a deploykit-render one).

func NormalizeCgroupSize added in v0.2026194.615

func NormalizeCgroupSize(s string) string

NormalizeCgroupSize converts a podman-style size ("6g") to systemd's required uppercase suffix ("6G"); systemd silently rejects lowercase on Memory* caps.

func ParentDirForDest added in v0.2026192.1709

func ParentDirForDest(dest string) string

ParentDirForDest returns the parent directory of a copy/write destination, or "" when it is the root or current dir (no auto-mkdir needed).

func PathLeaf added in v0.2026205.1558

func PathLeaf(path string) string

PathLeaf returns the last segment of a dotted deployment path. "foo.bar.baz" -> "baz"; "foo" -> "foo"; "" -> "". Unlike SplitDottedPath, a malformed path (leading/trailing/doubled dots) still yields its raw trailing segment rather than nil — callers that only care about the LEAF (e.g. a "host"/"local" literal-name check) want the tolerant form. Shared by the host-side deploy dispatcher and candy/plugin-bundle's node-classification helpers (R3 — one source).

func PersistBedDeployOverrides added in v0.2026202.1112

func PersistBedDeployOverrides(name string, node BundleNode, externalInPlace bool, marshalNode func(name string, node *BundleNode) (*yaml.Node, error))

PersistBedDeployOverrides seeds the per-host charly.yml with a kind:check bed's project-declared deploy-shaped fields (port / volume / env / tunnel / security / network), its disposable/lifecycle classification, AND its resource-arbitration role (preemptible holder / requires_exclusive / requires_shared claimant), BEFORE the bed's `charly config` step runs. Seeding the arbiter fields is what lets a bed/deploy MEMBER be an arbiter participant: bringUpMembers persists each member here, then the member's `charly start` reloads the per-host node and fires the arbiter off these fields — without them a member's requires_exclusive reloaded as [] and the arbiter silently no-op'd. The folded bed node is the source of truth, but `charly bundle add` / `charly config` otherwise source those fields from the IMAGE LABELS and gate port writes behind an operator `-p` — so a bed's declared `port:` remap would never reach the quadlet (it would fall back to the image default and collide with any same-image deploy already bound to that port). Seeding the per-host entry up front lets the existing MergeDeployOntoMetadata → quadlet path honor the overrides with no new merge logic; `charly config`'s own SetPorts-gated save then leaves the seeded port untouched (it passes no `-p`). SaveDeployState's per-field guards make unset bed fields no-ops, so this is safe for beds that declare only a subset.

externalInPlace is the caller-computed isExternalDeploySubstrate(node.Target) result — this package cannot query the live provider registry itself (see file header).

func PixiBoundCandies added in v0.2026193.1801

func PixiBoundCandies(layers map[string]CandyModel) map[string]bool

PixiBoundCandies identifies candies that have install files (user.yml/root.yml) but depend on a pixi environment from their including parent meta-candy. These candies must NOT be extracted into auto-intermediates because the intermediate won't have the pixi environment they need.

A candy is pixi-bound if: 1. It has install files (user.yml or root.yml) 2. It does NOT have its own pixi manifest (pixi.toml/pyproject.toml/environment.yml) 3. It is included via candy: by another candy that DOES have a pixi manifest

func PodAwareEnvProvides added in v0.2026190.848

func PodAwareEnvProvides(entries []spec.EnvProvideEntry, consumerKey, ctrName string) []spec.EnvProvideEntry

PodAwareEnvProvides rewrites same-deploy env values to localhost and dedups cross-deploy entries by name (local wins).

func PodmanSecretExists added in v0.2026202.105

func PodmanSecretExists(engine, name string) bool

PodmanSecretExists checks whether a podman secret with the given name already exists.

func PortMapFromMappings added in v0.2026198.358

func PortMapFromMappings(mappings []string) map[int]int

PortMapFromMappings builds a {containerPort -> hostPort} lookup table from the resolved port mapping list. Mappings that don't parse are silently skipped (the loud-skip warning lives in kit.CheckPortAvailability).

func PreemptEffectiveRestore added in v0.2026190.848

func PreemptEffectiveRestore(p *PreemptibleConfig) string

EffectiveRestore returns the configured restore policy with the default.

func PreemptEffectiveStop added in v0.2026190.848

func PreemptEffectiveStop(p *PreemptibleConfig) string

EffectiveStop returns the configured stop mechanism with the default.

func PrimaryDistroTag added in v0.2026190.848

func PrimaryDistroTag(img *ResolvedBox, hostCtx HostContext) string

PrimaryDistroTag picks the distro tag this plan is materialized against. img.Distro is the AUTHORITATIVE deploy-target distro chain and always wins: the caller's synthetic-box construction sets the operator host's tags for a host target / the GUEST's for a vm target (candy/plugin-bundle/candy_select.go's syntheticHostBoxFromEnvelope / syntheticVmBoxFromEnvelope, K4 unit B), and ResolveBox the image's — so a vm deploy resolves the guest distro, a host deploy the operator's, a pod/image deploy the image's. For a host deploy img.Distro[0] == hostCtx.Distro (both from DetectHostDistro; PrimaryTag() == Tags[0]), so this is byte-identical to the old host path while fixing vm deploys (whose hostCtx.Distro is the OPERATOR's distro, NOT the guest's — the detectHostContext default). hostCtx.Distro is only a fallback when img carries no distro. (Package resolution already uses img via CompileSystemPackageSteps, so making the plan's distro img-authoritative also removes a latent inconsistency, never introduces one.)

func ProjectBoxAggregates added in v0.2026209.2002

func ProjectBoxAggregates(cfg *spec.Config, layers map[string]spec.CandyReader, name string, resolved *buildkit.ResolvedBox, view *spec.ResolvedBoxView)

ProjectBoxAggregates fills the box-AUTHORED + box-AGGREGATE fields on a ResolvedBoxView from the authored BoxConfig + the cross-candy collectors. The authored surfaces (Plan, AuthoredAliases) come from cfg.BoxConfig(name) and are SKIPPED for auto-intermediate boxes (which have no authored config). The aggregates (Ports/Volumes/Aliases/Engine) read cfg+layers by name and work for authored boxes AND intermediates — render-prep's buildBakedMetadata already used the same collectors for every gen.Box. A collector error leaves that aggregate empty (a read-only projection never fails the whole load). Shared by the pre-resolved (build-prep), fresh-resolve (validate), and auto-intermediate passes (R3).

func ProjectResolvedBox added in v0.2026209.2002

func ProjectResolvedBox(b *buildkit.ResolvedBox) spec.ResolvedBoxView

ProjectResolvedBox projects a resolved box (buildkit.ResolvedBox) into the wire-safe spec.ResolvedBoxView: EXACTLY the non-json:"-" fields `charly box inspect` already serializes (json.MarshalIndent(*ResolvedBox)), in declaration order. The 6 json:"-" host-only compute caches are DROPPED — they are re-derivable by a resolving plugin (or reached via RunHostStep), never wire data (S-K5 verdict, the design key).

func PromptPassword added in v0.2026202.105

func PromptPassword(prompt string) (string, error)

PromptPassword reads a password from the terminal without echo — the interactive secret-entry path in ProvisionPodmanSecrets (a `charly config`-time operator prompt).

func ProvisionData added in v0.2026198.345

func ProvisionData(engine string, imageRef string, meta *spec.BoxMetadata,
	bindMounts []ResolvedBindMount, namedVolumes []spec.VolumeMount,
	deployName, instance string, mode DataProvisionMode) (int, error)

ProvisionData copies data from image staging (/data/) into the runtime volumes — both bind-mounted host directories AND podman named volumes. It matches data entries from image metadata against the resolved volume list by bare volume name.

Volume kind determines the seeding command: bind mounts use the host path as the -v source with --userns=keep-id, named volumes use the full volume name as the -v source WITHOUT keep-id (the rootless subuid mapping handles ownership for named volumes).

For data images (FROM scratch, no shell), named-volume targets use podman's --mount type=image to expose the staging filesystem and a lightweight helper image (busybox) to run cp. Bind-mount targets use the simpler podman create + podman cp path.

func PruneContainerInitForSystemd added in v0.2026209.2002

func PruneContainerInitForSystemd(order []string, hostCtx HostContext) []string

PruneContainerInitForSystemd drops the `supervisord` candy (the CONTAINER init system) from a resolved DEPLOY candy order when the target is systemd (a host/vm MachineVenue compile). On a systemd target the OS init is the one and only init system — every candy's `service:` entries render as systemd units — so pulling in supervisord is wrong (it lands installed-but-unused, a second init). Pod/k8s deploys and OCI image builds keep supervisord (it IS their init), so this only affects host/vm deploys. Candies that `require: supervisord` purely for graph ordering are unaffected at runtime — their services run under systemd regardless of whether the supervisord package is present.

Relocated from charly/bundle_add_cmd.go (K4 unit B, core-min wave 3) — a pure function of order+HostContext.MachineVenue with no core-only dependency, now shared (R3) by charly-core's compileBoxSelection (unchanged, box shape) AND candy/plugin-bundle's candy-selection path (K4 unit B) instead of being duplicated on both sides.

func PruneStaleVmDottedTwin added in v0.2026202.1522

func PruneStaleVmDottedTwin(dc *BundleConfig, canonicalKey string) string

PruneStaleVmDottedTwin removes and returns any OTHER dc.Bundle key that is a dotted deploy name whose VmDomainIdentity sanitizes to the SAME domain identity as canonicalKey (also VmDomainIdentity-sanitized) — the "stale twin" a prior version's now-eliminated dotted-key vm-state write could leave behind: a nested (dotted) deploy's per-host state used to ALSO get written under its raw, unsanitized name, racing the canonical "vm:"+VmDomainIdentity(name)-keyed write, and poisoning the whole overlay on every subsequent load since a dotted key fails the loader's deployment-name validation. Pulled out as its own pure function purely for testability. Returns "" when no twin is found.

func QuadletDir added in v0.2026198.345

func QuadletDir() (string, error)

QuadletDir returns the user-level quadlet directory.

func QuadletExists added in v0.2026198.345

func QuadletExists(boxName string) (bool, error)

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

func QuadletExistsInstance added in v0.2026198.345

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

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

func QuadletFilename added in v0.2026198.345

func QuadletFilename(boxName string) string

QuadletFilename returns the quadlet filename for an image.

func QuadletFilenameInstance added in v0.2026198.345

func QuadletFilenameInstance(boxName, instance string) string

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

func RawCandyPair added in v0.2026209.2002

func RawCandyPair(r spec.CandyReader) (spec.CandyModel, spec.CandyView, bool)

RawCandyPair returns the underlying (spec.CandyModel, spec.CandyView) pair a wrapped spec.CandyReader carries — the W9 escape hatch (specCandyAdapter.RawCandy(), reached via a structural type assertion). ok is false only for a CandyReader implementer that isn't NewSpecCandyModel's adapter (no such implementer exists in production; a defensive, never-panicking fallback for the theoretical case).

func RegisterDeployStateHost added in v0.2026196.928

func RegisterDeployStateHost(h *StateHostMechanisms)

RegisterDeployStateHost is the charly init hook (called once from package main at startup).

func RejectImageRefAsDeployName added in v0.2026190.848

func RejectImageRefAsDeployName(box string) error

RejectImageRefAsDeployName fails a deploy-CREATION command (config setup / start) whose positional is a tagged registry image ref used AS the deploy NAME. The ref's registry-host dots make an invalid charly.yml deploy key (dots are reserved for dotted-path addressing), so the deploy would save and the NEXT config load would hard-fail (the 2026-07 `charly config ghcr.io/…:tag` corruption). A registry image needs an explicit short deploy name. Gated on BOTH a dot (invalid key) AND an image `:tag` (so a github repo ref, which pins with @version, and a bare dotted-path address are untouched).

func RelativeCandySequence added in v0.2026193.1801

func RelativeCandySequence(boxName string, parentProvided map[string]bool, boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel, globalOrder []string, pixiBound map[string]bool) []string

RelativeCandySequence returns an image's candies minus what the parent provides, ordered according to the global candy order.

func RemoveBoxDeploy added in v0.2026193.1222

func RemoveBoxDeploy(dc *BundleConfig, boxName string)

RemoveBoxDeploy removes an image's entry from a deploy config.

func RemoveByExactSource added in v0.2026196.928

func RemoveByExactSource[T Named](entries []T, source string) ([]T, bool)

RemoveByExactSource removes entries whose source matches the exact deploy key (no cross-instance match). Relocated from charly/provides.go.

func RemoveBySource added in v0.2026196.928

func RemoveBySource[T Named](entries []T, boxName string) ([]T, bool)

RemoveBySource removes all entries injected by the given image (same base, any instance). Returns the filtered list and whether anything was removed. Relocated from charly/provides.go.

func RemoveEncryptedVolumes added in v0.2026202.105

func RemoveEncryptedVolumes(boxName, instance string)

RemoveEncryptedVolumes deletes the gocryptfs cipher/plain dirs for the deploy's encrypted volumes at `charly remove --purge`. removeVolumes handles named podman volumes, but an encrypted volume is a filesystem directory under the encrypted storage path, NOT a podman volume — so without this a purged disposable enc bed leaves an orphaned, credential-less cipher dir behind, and the next deploy fails to mount it ("cipher: message authentication failed", the fresh passphrase no longer matching the stale master key).

It enumerates the on-disk dirs by the deploy's `charly-<storageDir>-` prefix (the same per-deploy prefix removeVolumes filters podman volumes by) rather than via LoadEncryptedVolume, so it works even when the deploy config is already gone (the orphaned-after-a-crash case is exactly when the dir persists). Each mount is unmounted best-effort before removal; a purge never hard-fails on cleanup.

func RemovePodmanSecrets added in v0.2026202.105

func RemovePodmanSecrets(engine string, secrets []CollectedSecret)

RemovePodmanSecrets removes podman secrets for an image (best-effort).

func RemoveVmDeployEntry added in v0.2026209.2002

func RemoveVmDeployEntry(deployName string, acquireLock func() (func() error, error), save func(*BundleConfig) error) error

RemoveVmDeployEntry strips deploy.<deployName> from charly.yml. acquireLock/save are the SAME injected host-resident primitives SaveVmDeployState takes — see this file's header.

func RenderBuilderScript added in v0.2026198.914

func RenderBuilderScript(s *BuilderStep, hostHome string) (string, error)

RenderBuilderScript turns a BuilderStep into the bash script that runs inside the builder container — the host-side (deploy) analog of the build-time multi-stage, fully config-driven: it renders the builder's phase.install.host cell via the SAME RenderTemplate engine (text/template). HOME/PIXI_CACHE_DIR/NPM_CONFIG_PREFIX/CARGO_HOME are injected by BuilderRunOpts.Env before the script starts. Shared by BuildDepPkgsOnHost (this file) and charly/builder_venue.go's runVenueHomeArtifactBuilder (still core — needs the injected image-resolve/ensure seam, a genuine loader dependency).

func RenderDnfConfWrite added in v0.2026192.1709

func RenderDnfConfWrite(d *spec.Dnf) string

RenderDnfConfWrite renders the dnf download-tuning conf snippet (appended to /etc/dnf/dnf.conf before the bootstrap install). Relocated from charly (P8).

func RenderHostPackageCommand added in v0.2026201.1934

func RenderHostPackageCommand(distroCfg *buildkit.DistroConfig, s *SystemPackagesStep) (string, error)

RenderHostPackageCommand renders the host-venue package-install command for a SystemPackagesStep from the format's phase.install.host cell in the embedded vocabulary — the SAME PhaseTemplate + NewInstallContext + RenderTemplate path deploykit.OCITarget uses for the container venue (R3). No hardcoded dnf/apt/pacman dispatch: the format selects the template; the command is config-driven.

Returns ("", nil) when the step is not an install-phase step, has no packages, or the format declares no host cell — all "nothing to run", not errors. A missing DistroConfig / format definition IS an error (the deploy can't honor a package step it can't render).

func RenderLocalPkgImageInstall added in v0.2026198.914

func RenderLocalPkgImageInstall(s *LocalPkgInstallStep, devLocalPkg bool, imageDir, boxName string) (string, error)

RenderLocalPkgImageInstall emits the IMAGE-build install of a candy's `localpkg:` package. It is the ONE place the check-vs-production charly-binary distinction lives (R3 — shared by every build-mode image-install path so it can never drift):

  • PRODUCTION boxes (devLocalPkg=false) DOWNLOAD the candy's PUBLISHED package (LocalPkgDef.DownloadTemplate → releases/latest, ${ARCH} resolved by BuildKit) and install it. A real box ships the latest RELEASED toolchain.

  • DISPOSABLE EVAL BEDS (devLocalPkg=true) BUILD the candy's package from the LOCAL in-development source (LocalPkgDef.BuildTemplate, via the SAME BuildLocalPkgOnHost the deploy path uses — R3), stage it into the image build context, and COPY+install it. A bed thus ALWAYS tests the in-development charly, never a stale published release.

Both modes install via the SAME dep-resolving InstallTemplate (pacman -U / dnf install / apt-get install), so the toolchain is OS-tracked either way. Returns "" (no directive) when the format declares no localpkg contract (the candy's own task: install is the fallback).

func RenderNamingPattern added in v0.2026201.1934

func RenderNamingPattern(pattern, source, id string) (string, error)

RenderNamingPattern fills in {{.Source}} and {{.UUID6}} variables.

func ResolveBedCheckLevel added in v0.2026202.1112

func ResolveBedCheckLevel(hasResolvedBox bool, checkLevel string) string

ResolveBedCheckLevel resolves the acceptance-depth rung for a bed. hasResolvedBox is true iff the bed's root carries an `image:` AND that image resolved to a box config; checkLevel is that box's authored check_level (ignored when hasResolvedBox is false). VM / local beds carry no box image, so they always run at the default rung — the caller (which alone can resolve a box ref against the loaded project) computes hasResolvedBox/checkLevel and calls this pure classifier.

func ResolveBoxEngine added in v0.2026209.2002

func ResolveBoxEngine(cfg *spec.Config, layers map[string]spec.CandyReader, boxName string, globalRunEngine string) string

ResolveBoxEngine returns the run engine for a specific box: candy-level engine requirements (transitive closure) win over the global default. Deploy-time overrides come from BundleNode.Engine via ResolveBoxEngineForDeploy / ResolveBoxEngineFromMeta. Relocated from charly/engine.go (build-side resolve; distinct from the deploy-side ForDeploy/FromMeta twins).

func ResolveBoxEngineForDeploy added in v0.2026198.345

func ResolveBoxEngineForDeploy(boxName, instance, globalEngine string) string

ResolveBoxEngineForDeploy resolves the run engine from the per-host deploy config, falling back to globalEngine. No charly.yml (project) dependency.

func ResolveBoxEngineFromMeta added in v0.2026198.345

func ResolveBoxEngineFromMeta(meta *spec.BoxMetadata, globalEngine string) string

ResolveBoxEngineFromMeta returns the engine from image metadata labels, falling back to globalEngine if not set.

func ResolveBoxLevels added in v0.2026192.1709

func ResolveBoxLevels(boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) ([][]string, error)

ResolveBoxLevels resolves box dependencies and returns them grouped by build level. Boxes at the same level can be built concurrently.

func ResolveBoxOrder added in v0.2026192.1709

func ResolveBoxOrder(boxes map[string]*buildkit.ResolvedBox, layers map[string]CandyModel) ([]string, error)

ResolveBoxOrder resolves box dependencies and returns them in build order. Boxes that reference other boxes via `base` create dependencies. Each box's Builder field determines its builder dependency. Pass candies to enable conditional builder dependency; nil for unconditional.

func ResolveCandyOrder added in v0.2026192.1709

func ResolveCandyOrder(requested []string, layers map[string]CandyModel, parentCandies map[string]bool) ([]string, error)

ResolveCandyOrder resolves candy dependencies and returns them in topological order. It takes the explicitly requested candies and the full candy map, then: 1. Expands candy composition (candy: field) 2. Transitively resolves all dependencies 3. Topologically sorts the result 4. Returns candies in install order (dependencies before dependents)

parentCandies contains candies already installed by parent boxes (via base chain). These are excluded from the returned order.

func ResolveCandySecret added in v0.2026209.2002

func ResolveCandySecret(layer spec.CandyReader, cred CredentialAccess) map[string]string

ResolveCandySecret walks the candy's secret_requires + secret_accepts and resolves each against the credential store (via cred). Required entries that miss everywhere auto-generate a 32-byte hex token (see EnsureCandySecret). Optional secret_accepts: entries that miss fall back to dep.Default.

Returns the env map; never returns an error. The auto-generate policy guarantees every secret_requires: resolves to a non-empty value. Takes spec.CandyReader (the read-only interface every scanned candy is wrapped into) rather than a concrete type — this function needs only the SecretRequire/SecretAccept accessors.

func ResolveCascadePackages added in v0.2026190.848

func ResolveCascadePackages(layer CandyModel, img *ResolvedBox) (pkgs []string, raw map[string]any, matched bool)

func ResolveContainer added in v0.2026198.345

func ResolveContainer(box, instance string) (engine, name string, err error)

ResolveContainer resolves engine + container name, verifying the container is running. Use "." as image name for local mode (returns empty engine and name).

func ResolveDeployChain added in v0.2026190.848

func ResolveDeployChain(roots map[string]BundleNode, dotted string, root DeployExecutor) (*BundleNode, DeployExecutor, error)

ResolveDeployChain walks `dotted` through `roots` (typically the merged deployment tree from resolveTreeRoot) and returns the leaf BundleNode + a composed DeployExecutor chain that reaches it from `root`.

`root` is typically &ShellExecutor{} (the operator's host, or the harness-sandbox context the harness loop runs in). Pass nil to substitute ShellExecutor.

For each path segment, a single hop is added based on the node's target classification:

target: pod / container → NestedExecutor with JumpPodmanExec /
                          JumpDockerExec into "charly-<flat-path>".
                          Container name flattens dot-separated
                          paths to underscore-separated to remain
                          a legal podman container name.
target: vm              → plain SSHExecutor when the parent chain
                          is local (no wrapper overhead), otherwise
                          NestedExecutor with JumpSSH on top.
target: host            → no hop (host nodes share the parent
                          venue).
target: k8s             → error (k8s manifests are leaves; not
                          traversable as exec targets).

Returns clear errors with available-name hints when a segment fails to resolve.

func ResolveDeployForwards added in v0.2026202.105

func ResolveDeployForwards(authored []string, alloc map[string]int) ([]string, error)

ResolveDeployForwards maps authored network.port_forwards entries to concrete "<host>:<guest>" strings: an `auto:<guest>` entry resolves to its persisted auto-allocated host port, and a fixed "<host>:<guest>" passes through unchanged. An `auto:<guest>` with NO persisted allocation is a LOUD ERROR, never a silent drop: the caller runs this only POST-vm-create, where the allocation MUST exist, so a miss means a persist/read key mismatch — surfacing it here turns a confusing downstream `connection refused` into a diagnostic naming the unresolved entry (R1/R4). Pure; unit-tested. Reuses vmshared.SplitPortForward (R3) for the "<host>:<guest>" split, the same helper vmshared's own QEMU argv renderer uses.

func ResolveEncPassphrase added in v0.2026202.1112

func ResolveEncPassphrase(boxName string, autoGenerate bool, cred CredentialAccess) (string, error)

ResolveEncPassphrase resolves the gocryptfs passphrase for an image. Resolution order: GOCRYPTFS_PASSWORD env var → credential store (keyring/config) → auto-generate or interactive prompt.

func ResolveEncPassphraseForMount added in v0.2026202.1112

func ResolveEncPassphraseForMount(boxName, backend string, cred CredentialAccess, reset func(), waiter func(ctx context.Context, boxName string, resolver func() (string, string), reset func()) (string, string, error)) (string, error)

ResolveEncPassphraseForMount resolves the gocryptfs passphrase with backend-aware and failure-aware retry behavior.

Under systemd (INVOCATION_ID set) with a keyring-capable backend:

  • If the store is temporarily locked ("locked") or unreachable ("unavailable"), retry every EncMountPollPeriod until EncMountDeadline elapses, then fail with a clear diagnostic.
  • If the store answered and the credential is NOT stored ("default"), fail immediately with an actionable error — no amount of polling will conjure a credential that was never stored.

Explicit non-keyring backends under systemd: try resolve once, fail fast if not found. No polling.

Interactive callers fall back to ResolveEncPassphrase which can prompt.

backend/reset/waiter are supplied by the caller: charly-core passes its resolveSecretBackend()/resetDefaultCredentialStore/awaitKeyringUnlockViaPlugin (the keyring-unlock wait RPCs verb:credential `await-unlock` out-of-process in candy/plugin-secrets); a future plugin caller supplies its own InvokeProvider-backed equivalents. waiter may be nil (falls through to the bounded retry).

func ResolveEncPassphraseForMountWithResolver added in v0.2026202.1112

func ResolveEncPassphraseForMountWithResolver(
	boxName, backend string,
	resolver func() (value, source string),
	reset func(),
	waiter func(ctx context.Context, boxName string, resolver func() (string, string), reset func()) (string, string, error),
) (string, error)

ResolveEncPassphraseForMountWithResolver is the testable core of ResolveEncPassphraseForMount. It accepts a resolver closure, a reset closure, and a waiter closure so tests can supply mock implementations without touching global state, environment variables, or DBus.

func ResolveEncVolumeDir added in v0.2026202.105

func ResolveEncVolumeDir(vol vmshared.DeployVolumeConfig, defaultStoragePath, boxName string) string

ResolveEncVolumeDir returns the volume directory for an encrypted volume. If the volume has an explicit Host path, use it directly. Otherwise, use the global default: <storagePath>/charly-<image>-<name>.

func ResolveHookSecretEnv added in v0.2026202.1112

func ResolveHookSecretEnv(boxName, instance string, meta *spec.BoxMetadata, credServiceVNC string, cred CredentialAccess) []string

ResolveHookSecretEnv returns `NAME=value` entries for every secret_accept / secret_require value that resolves from the credential store, so lifecycle hooks (post_enable / pre_remove) receive credential-backed secrets EXPLICITLY via `podman exec -e`. This is load-bearing: the CLI `-e` form of these secrets is scrubbed from c.Env by charly-core's scrubSecretCLIEnv (never plaintext in charly.yml), and a podman `type=env` secret is not reliably inherited by `podman exec`, so a hook that consumes a secret (e.g. github-runner's registration token) would otherwise never see it. Generic across every hook+secret candy (R3); inert (returns nil) when the image declares no secrets or none resolve.

func ResolveLocalPkgDir added in v0.2026198.914

func ResolveLocalPkgDir(ref, candyDir, projectDir, sentinel string) string

ResolveLocalPkgDir locates the package SOURCE directory for a candy's `localpkg:` hint. Resolution order, returning the first directory that actually contains a `PKGBUILD` file:

  1. absolute ref → used verbatim.
  2. <candyDir>/<ref> — the source bundled alongside the candy.
  3. <projectDir>/<ref> — relative to the deploy project dir (os.Getwd).
  4. walk UP from projectDir, trying <ancestor>/<ref> at each level — this is the operator path: `charly -C box/cachyos deploy add cachyos-gpu` has a project dir of box/cachyos while pkg/arch lives at the SUPERPROJECT root (../../pkg/arch). The walk finds it without the candy needing to know how deeply the consuming project is nested.

Returns "" when no PKGBUILD is found anywhere — the caller treats that as a no-op (the candy's own curl/COPY task is the documented fallback).

The SOURCE-dir marker is the format's `source_sentinel` (PKGBUILD for pac, *.spec for rpm, debian/control for deb), matched via filepath.Glob so a plain filename, a sub-path, or a glob all work — no hardcoded format literal here.

func ResolveNodePath added in v0.2026190.848

func ResolveNodePath(roots map[string]BundleNode, path string) (*BundleNode, []*BundleNode, error)

ResolveNodePath walks roots[path0].Children[path1]...[pathN] and returns the targeted node plus its parent chain (root-first, excluding the target itself). Returns a descriptive error when any path segment is missing so the user sees which segment doesn't exist.

An empty path is invalid — callers dispatch to WalkPreOrder/WalkPostOrder against a named root instead of resolving "".

func ResolveSecretForCandy added in v0.2026209.2002

func ResolveSecretForCandy(layers []spec.CandyReader, cred CredentialAccess) map[string]string

ResolveSecretForCandy is the batch variant used when multiple candies in a single deploy share secret_requires — their resolution results merge into one env map, with candy-order precedence (later candies win on duplicate names, matching generate.go's own secretRequiresMap semantics in the label-emission path).

func ResolveSecretValue added in v0.2026202.1112

func ResolveSecretValue(s CollectedSecret, boxName, instance, credServiceVNC string, resolve CredentialResolver) (value, source string)

ResolveSecretValue looks up the value for a secret from the credential store.

When CollectedSecret.Service and CollectedSecret.Key are both non-empty, they take precedence over the default lookup chain: the credential store is queried exactly at (Service, Key) with the Env var as the env override. This is the path used by secret_accepts / secret_requires entries synthesized by CollectCandySecretAccepts, where the candy author may have set `key: charly/api-key/openrouter` to point at a shared credential namespace.

When Service/Key are unset, the default chain (used by candy-owned secrets) applies: env var → charly/secret/<podman-name> → charly/secret/<bare-secret-name>.

func ResolveSidecarContainer added in v0.2026198.345

func ResolveSidecarContainer(box, instance, sidecar string) (engine, name string, err error)

ResolveSidecarContainer resolves engine + container name for a named sidecar, verifying it is running.

func ResolveTemplate added in v0.2026198.358

func ResolveTemplate(tmpl, containerName string, portMap map[int]int) string

ResolveTemplate replaces template placeholders in a string:

{{.ContainerName}}        -> containerName
{{.ContainerPort <N>}}    -> <N> (literal — kept for symmetry/readability)
{{.HostPort <N>}}         -> host port mapped to container port <N>
                             (looked up in portMap; falls back to <N>
                             if not found — caller should validate the
                             port is actually published before relying
                             on the substitution)

portMap is a {containerPort -> hostPort} table built from the resolved port mapping list at env-injection time. nil portMap is accepted (every {{.HostPort N}} degrades to the literal container port — useful for validation-time substitution before runtime data is available).

func ResolveTunnelConfig added in v0.2026202.1522

func ResolveTunnelConfig(t *spec.TunnelYAML, boxName string, dns string, _ map[string]spec.CandyReader, _ []string, portProtos map[string]string, boxPorts []string) *spec.TunnelConfig

ResolveTunnelConfig resolves a TunnelYAML into a TunnelConfig with defaults applied. portProtos maps container port -> protocol ("http" or "tcp") from candy PortSpec data. boxPorts is the list of image port mappings (e.g. "18789:18789", "443:18789").

func ResolveUserSpec added in v0.2026192.1709

func ResolveUserSpec(userField string, img *buildkit.ResolvedBox) (directive, chown string)

ResolveUserSpec converts a task's `user:` field to (userDirective, chownPair).

func ResolveVmSshPort added in v0.2026202.1522

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

ResolveVmSshPort picks the host-side SSH port forward, reusing the persisted vm_state.ssh_port (idempotent across rebuilds) when ssh.port_auto is set. The project-config READ is the one deploykit-coupled bit (LoadDeployConfigForRead over the per-host overlay); the resolution/allocation decision itself is the shared kit.ResolveVmSshPort.

func ResolveVolumeBacking added in v0.2026194.615

func ResolveVolumeBacking(boxName, instance string, labelVolumes []VolumeMount, deployVolumes []vmshared.DeployVolumeConfig, home string, encStoragePath string, volumesPath string) ([]VolumeMount, []ResolvedBindMount)

ResolveVolumeBacking splits image volumes into named volumes and bind mounts based on the deploy's charly.yml volume configuration. Volumes without a deploy override stay named volumes; a type=bind/encrypted override becomes a ResolvedBindMount; deploy-only volumes (carrying Path, absent from any label) are added as binds. PURE — the host resolves labelVolumes (ExtractMetadata) + deployVolumes (charly.yml) and calls this in the config-resolve seam; the pod plugin consumes the result.

func ResolveWorkingDir added in v0.2026194.615

func ResolveWorkingDir(volumes []VolumeMount, bindMounts []ResolvedBindMount, home, boxName, instance string) string

ResolveWorkingDir returns the container working directory — the "workspace" volume/bind's container path if declared, else home.

func ResourceCapArgs added in v0.2026198.345

func ResourceCapArgs(sec spec.SecurityConfig) []string

ResourceCapArgs returns the podman run flags for memory and CPU caps. Emitted identically in both the privileged and non-privileged branches of SecurityArgs because privileged containers still need resource limits.

func RestartPodService added in v0.2026201.1135

func RestartPodService(boxName, instance string) error

RestartPodService restarts a service container. In quadlet mode it issues a single `systemctl --user restart`, which is atomic from systemd's perspective — ExecStopPost (e.g. tailscale serve --off) runs before ExecStartPost (tailscale serve), and the unit ends in either active or failed, never the silent stopped state a manual stop+start sequence can produce when start fails. Direct mode delegates to the resolved engine's restart. Used by `charly restart`.

func RetrieveCandyArtifacts added in v0.2026201.1934

func RetrieveCandyArtifacts(
	ctx context.Context,
	exec spec.DeployExecutor,
	layers []spec.CandyReader,
	deployName string,
	envVars map[string]string,
	opts spec.EmitOpts,
	readiness vmshared.ResolvedReadiness,
) error

RetrieveCandyArtifacts walks every artifact declared by every candy included in the deploy and pulls it back via the executor's GetFile. Missing non-optional files are a hard error (R1).

deployName is the deploy-yml name (e.g., "vm:k3s-srv") — exposed to rewrite-path expansion as ${deploy_name}. envVars is an additional substitution context (e.g., K3S_SERVER_HOSTNAME from the deploy.env block, used to rewrite server URLs in a retrieved kubeconfig). readiness is the pre-resolved WaitCapped bound source (see the file header).

func RetryUnavailable added in v0.2026202.1112

func RetryUnavailable(
	boxName, backend string,
	resolver func() (string, string),
	reset func(),
) (string, error)

RetryUnavailable polls the resolver with a bounded deadline for transient backend-probe failures (source="unavailable").

func RewriteServerPorts added in v0.2026202.105

func RewriteServerPorts(data string, guestToHost map[string]string) string

RewriteServerPorts rewrites every `https://<host>:<guestPort>` in data to `https://127.0.0.1:<hostPort>` for each guest->host mapping (the QEMU user-mode forward lives on the host loopback). Pure; unit-tested.

func RunVenueBuilderStep added in v0.2026209.2002

func RunVenueBuilderStep(ctx context.Context, exec DeployExecutor, venueHome string, resolveImage func(string) (string, error), ensureImage func(context.Context, string) error, s *BuilderStep, opts EmitOpts) error

RunVenueBuilderStep builds a BuilderStep on the HOST and installs the artifacts onto the venue the executor addresses. Routes by OUTPUT shape, not builder name: a builder that produces package FILES carries the format's local_pkg contract (s.LocalPkg, set by the compiler for the aur builder) and goes through the build-on-host -> transfer -> package-install leg; everything else is a home-artifact builder (pixi/npm/cargo) whose ~/.pixi / ~/.npm-global / ~/.cargo output is tarred into the venue home. An unknown builder with neither shape has no host build script (RenderBuilderScript errors on a nil vmshared.BuilderDef cell); --skip-incompatible skips it.

resolveImage/ensureImage are the injected image-resolve/ensure closures (the SAME shape BuildDepPkgsOnHost already takes) — the caller's ONE genuine core dependency (a project Config + dir to resolve a short/namespace-qualified builder image and fall back to a local `charly box build`).

func RunVenueHomeArtifactBuilder added in v0.2026209.2002

func RunVenueHomeArtifactBuilder(ctx context.Context, dexec DeployExecutor, venueHome string, resolveImage func(string) (string, error), ensureImage func(context.Context, string) error, s *BuilderStep, opts EmitOpts) error

RunVenueHomeArtifactBuilder runs a user-home builder (npm/pixi/cargo) on the HOST into a staging dir bind-mounted AS the venue home, then ships the produced home subdirs into the venue user's $HOME over the executor.

The critical move is running the builder with HOME = the VENUE home PATH (venueHome). npm shebangs, cargo binary rpaths, and pixi env activation scripts bake the install-prefix path; baking the venue's home means the artifacts work unchanged once extracted into the venue's real $HOME. Build caches (.cache/) are excluded from the transfer — they're large and the venue doesn't need them.

func SanitizeUnitName added in v0.2026201.1934

func SanitizeUnitName(s string) string

SanitizeUnitName makes a string safe for systemd unit naming (replaces / and . with -).

func SaveBundleConfig added in v0.2026196.928

func SaveBundleConfig(dc *BundleConfig, marshalNode func(name string, node *BundleNode) (*yaml.Node, error)) error

SaveBundleConfig writes a BundleConfig to the standard charly.yml path. The kind-blind file shell: the HEAD version stamp (kit.LatestSchemaVersion), the provides directive, the caller-supplied per-entry node-form bodies, and the atomic tempfile+os.Rename write with a fail-safe re-check. The deploy-kind-specific marshal (the struct-body → compact node-form transform) is the caller's responsibility — supplied via marshalNode, which returns the MIGRATED node-form body for one BundleNode (the value placed under the entry's name key). This keeps SaveBundleConfig kind-blind: no per-kind map, no target-vocabulary switch, no deploy-kind-specific struct knowledge. Relocated from charly/deploy.go (K5-Unit-1); the LoadUnified hop (the fail-safe re-check) reaches core through DeployStateHost.

func SaveDeployState added in v0.2026196.928

func SaveDeployState(boxName, instance string, input SaveDeployStateInput, marshalNode func(name string, node *BundleNode) (*yaml.Node, error))

SaveDeployState persists deployment parameters to charly.yml (best-effort). Merges onto any existing entry to preserve fields from charly bundle import. Relocated from charly/deploy.go (K5-Unit-1); the process-shared flock is a kind-blind kit primitive (kit.AcquireFileLock on the deploy-config path) and the loader reaches core through the DeployStateHost seam (LoadUnifiedBundleConfig). marshalNode is the deploy-kind-specific node-form serializer the caller supplies (the callback SaveBundleConfig invokes per entry).

func SaveVmDeployState added in v0.2026209.2002

func SaveVmDeployState(deployName, vmEntity string, state *spec.VmDeployState, acquireLock func() (func() error, error), save func(*BundleConfig) error) error

SaveVmDeployState writes the updated VmDeployState into ~/.config/charly/charly.yml for the given deploy name. Idempotent — overwrites the deploy.<name>.vm_state block. vmEntity is the kind:vm entity the deploy targets ("" → derive from a legacy "vm:<name>" deployName prefix); it is persisted as the entry's `vm:` cross-ref so a bundle-keyed entry (a kind:check VM bed, whose deploy key e.g. `check-k3s-vm` differs from `vm:<entity>`) carries the linkage teardown needs to find + remove it.

acquireLock serializes the load→modify→save against concurrent charly processes — the SAME blocking deploy-config lock every other deploy-state writer uses. Without it two parallel `charly vm create` persist-auto-port writers (or a vm-create racing a `charly bundle add vm:<name>`) load → modify → save the shared ~/.config/charly/charly.yml and silently drop each other's entry.

func SchemeTarget added in v0.2026194.615

func SchemeTarget(scheme string, port int) string

SchemeTarget returns the backend URL for a tunnel scheme + port.

func ScopeVolumesToDeployKey added in v0.2026196.928

func ScopeVolumesToDeployKey(meta *spec.BoxMetadata, deployName, instance string)

ScopeVolumesToDeployKey renames meta's named-volume mounts from the image-derived prefix (charly-<image>-) to the deploy's own prefix (DeployVolumePrefix), so every distinctly-named deploy ALWAYS gets volume mounts distinct from any other deploy of the same image. No-op when the deploy's prefix already equals the image prefix (the common `charly config <image>` base deploy). Idempotent. Relocated from charly/deploy.go (reads *spec.BoxMetadata, no core dep — folds cleanly into the kit).

func SecretArgs added in v0.2026202.105

func SecretArgs(secrets []CollectedSecret) []string

SecretArgs returns --secret flags for container run (direct mode).

func SecretDeclaredOnBox added in v0.2026198.358

func SecretDeclaredOnBox(meta *spec.BoxMetadata) map[string]bool

SecretDeclaredOnBox returns the set of env var names an image declares as credential-backed (secret_accepts or secret_requires). Returns a non-nil empty set when meta is nil or has no secret declarations.

func SecretDepNames added in v0.2026198.358

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

SecretDepNames returns the flat list of env var names declared as credential-backed on an image. Used to populate deploykit.SaveDeployStateInput.SecretNames for the defense-in-depth scrub in saveDeployState. Returns nil (not an empty slice) when meta has no secret declarations — matches the rest of the omitempty-style API.

func SecretKeyForDep added in v0.2026198.358

func SecretKeyForDep(dep spec.EnvDependency) (service, key string)

SecretKeyForDep returns the (service, key) tuple used to look up a secret in the credential store. When the candy author set an explicit `key: charly/api-key/openrouter` override, that's parsed into its two segments; otherwise the default (charly/secret, dep.Name) is returned. The format is enforced by validateSecretDeps at build time, so this is purely a structural split — no validation is re-run here.

func SecurityArgs added in v0.2026198.345

func SecurityArgs(sec spec.SecurityConfig) []string

SecurityArgs returns the container run arguments for the given security config.

Note on the ShmSize+IpcMode interaction: podman rejects `--shm-size` when the IPC namespace is shared with the host (`--ipc=host`) because the host's /dev/shm is shared in-kernel and sized by the host kernel; an explicit shm-size on the container makes no sense in that case and yields a runtime error like "cannot set shmsize when running in the {host} IPC Namespace". Same logic applies to the quadlet generator's ShmSize= directive elsewhere.

func ServiceEntryAppliesToDistro added in v0.2026190.848

func ServiceEntryAppliesToDistro(entry *ServiceEntry, distros []string) bool

ServiceEntryAppliesToDistro reports whether a service entry should render for the given distro tag chain. An entry with an EMPTY distro: list applies to EVERY distro (the backward-compatible default — every pre-existing candy's services keep rendering everywhere). A non-empty list restricts the entry to the named distros, matched against either a bare distro name ("debian") or a full versioned tag ("debian:13") anywhere in the chain. This is the service analogue of a check step's exclude_distros: — the mechanism that lets ONE candy carry per-distro-divergent packaged units (modular virtqemud.socket on Fedora/Arch vs monolithic libvirtd.socket on Debian/Ubuntu) without a <name>-host sibling candy (CLAUDE.md R3).

func ServiceName added in v0.2026198.345

func ServiceName(boxName string) string

ServiceName returns the systemd service name for an image.

func ServiceNameInstance added in v0.2026198.345

func ServiceNameInstance(boxName, instance string) string

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

func ServiceRenderDistros added in v0.2026190.848

func ServiceRenderDistros(img *ResolvedBox, hostCtx HostContext) []string

compileServiceSteps turns the candy's service declarations into IR steps. Preference order:

  1. Unified `services:` list (Task 6) — preferred when present. Each entry becomes either a ServicePackagedStep (use_packaged:) or a ServiceCustomStep (full spec).

compileServiceSteps — unified schema only. Each ServiceEntry becomes either a ServicePackagedStep (use_packaged:) or a ServiceCustomStep (custom exec). Legacy fields (raw-INI service:, system_services:) are gone — external candies must run `charly migrate`.

**Init-system polymorphism filter (2026-05).** When a candy declares the same service `name:` twice — once with `use_packaged:` and once with custom `exec:` (the mixed-entry polymorphism pattern documented in CLAUDE.md "Init-system polymorphism via mixed `service:` entries" and `/charly-build:layer` "Service Declaration") — the compiler picks ONE based on the target's init system:

  • systemd target (host / vm) → emit the packaged form, SKIP the custom-exec sibling (the packaged unit handles the daemon).
  • supervisord target (oci/pod build) → SKIP the packaged form (supervisord can't consume systemd units), emit the custom form.

For singleton entries (no mixed pair), each entry renders as-is on the matching init system; mismatched singletons (e.g. a lone `use_packaged:` entry on a supervisord target) are silently skipped.

For systemd targets, the compiler ALSO pre-populates UnitText/UnitPath on the ServiceCustomStep by calling RenderService, so executors don't need a runtime lazy-render step. This consolidates what used to live in three different places (the deleted in-proc VM-target lazy fallback, the OCI build's per-entry routing, and the legacy nothing-rendered path on the local deploy target) into ONE compile-time filter. ServiceRenderDistros returns the distro tag chain a service entry's distro: filter is matched against for a deploy target. img.Distro is the AUTHORITATIVE deploy-target chain and always wins (mirrors PrimaryDistroTag): the caller's synthetic-box construction sets the GUEST chain (e.g. ["debian:13","debian"]) for a vm target / the operator host's for a host target, ResolveBox the image's. Using hostCtx.Distro here was the bug that made a vm deploy filter services against the OPERATOR's distro (arch) instead of the guest's (debian) — detectHostContext defaults hostCtx.MachineVenue=true + the operator distro even for a vm deploy, so a hostCtx-wins rule mis-scoped every vm/pod service. For a host deploy img.Distro is the operator's own tag chain (a SUPERSET of the single PrimaryTag, so arch-derivative hosts like cachyos now also match `arch:` entries). hostCtx.Distro is only a fallback when img carries no distro.

func SidecarTemplatesOf added in v0.2026198.358

func SidecarTemplatesOf(dc *BundleConfig) map[string]json.RawMessage

SidecarTemplatesOf returns the project-root sidecar templates carried by a deploy config (nil-safe), as OPAQUE bodies. These extend/override the embedded set inside the sidecar plugin's OpResolve.

func SortByPopularity added in v0.2026193.1801

func SortByPopularity(s []string, popularity map[string]int)

SortByPopularity sorts by descending popularity, then lexicographic ascending.

func SortedEnvList added in v0.2026198.254

func SortedEnvList(env map[string]string) []spec.KeyValue

SortedEnvList returns env as a deterministically-ordered []spec.KeyValue (template iteration order — a Go map has none of its own).

func SortedKeys added in v0.2026193.1801

func SortedKeys(m map[string]*TrieNode) []string

SortedKeys returns sorted keys from a trie-node map.

func SortedNestedKeys added in v0.2026190.848

func SortedNestedKeys(children map[string]*BundleNode) []string

func SplitDottedPath added in v0.2026190.848

func SplitDottedPath(path string) []string

SplitDottedPath splits a dotted deployment path into segments. An empty input or a path with any empty segment (leading/trailing/ doubled dots) yields nil so callers can flag the error at their layer with the original offending path string.

func StageInlineContent added in v0.2026192.1709

func StageInlineContent(buildDir, contextRelPrefix, candyName, content string) (string, error)

StageInlineContent writes a write-task's inline content to <buildDir>/_inline/<candy>/<sha256> on disk and returns the build-context-relative path suitable for COPY. Writes are idempotent — repeated calls with identical content are no-ops via content-addressed filenames. Relocated from charly (P8).

func StartPodService added in v0.2026201.1135

func StartPodService(boxName, instance string) error

StartPodService starts an already-configured pod deployment — the quadlet service when one exists, else the existing stopped container via the resolved engine. Used by the resource arbiter to restore a preempted holder: the deployment's quadlet/container already exists (the holder was running before preemption), so this is a plain service/container start, not a full `charly start` re-config.

func StopPodService added in v0.2026201.1135

func StopPodService(boxName, instance string) error

StopPodService stops a running pod deployment — the quadlet service when one exists (always via systemctl, so podman-stop + Restart=always can't create a restart loop), else the container directly via the resolved engine with a fallback to the other engine. It performs NO tunnel/unmount side effects — callers layer those on. Used by `charly stop` and the resource arbiter's preemption path, which wants a bare, reversible service stop that leaves the holder's disk/container intact for restart.

func StringSliceFromYAML added in v0.2026190.848

func StringSliceFromYAML(v any) ([]string, bool)

StringSliceFromYAML coerces a YAML-decoded value into []string. The decoder produces []interface{} for sequences; we tolerate already- stringified slices for callers that pre-process.

func StripSecretEnvNames added in v0.2026190.848

func StripSecretEnvNames(env map[string]string, blocked []string) map[string]string

StripSecretEnvNames removes any KEY=VAL entries from env whose KEY is in the blocked list. The blocked list is expected to be short (one entry per secret_* declaration on the image), so a per-entry delete is fine.

func StripVersion added in v0.2026190.848

func StripVersion(ref string) (string, string)

StripVersion splits an @-prefixed ref into its bare form and pinned version. A non-@ ref returns (ref, "").

func SystemdUserDir added in v0.2026198.345

func SystemdUserDir() (string, error)

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

func TailscaleFlag added in v0.2026194.615

func TailscaleFlag(scheme string) string

TailscaleFlag returns the tailscale serve/funnel flag for a scheme.

func TaskCacheMounts added in v0.2026192.1709

func TaskCacheMounts(t vmshared.Op, img *buildkit.ResolvedBox) []string

TaskCacheMounts renders a task's candy-declared `cache:` paths as BuildKit cache-mount flags. Ownership follows the task's stage user (root → shared, non-root → uid/gid-owned).

func TaskCoalescesWith added in v0.2026192.1709

func TaskCoalescesWith(current, next vmshared.Op, currentVerb string) bool

TaskCoalescesWith returns true if next can be batched with current under the adjacent-coalescing rule: same verb, same user, both verbs support batching.

func TaskKnownNames added in v0.2026192.1709

func TaskKnownNames(vars map[string]string) map[string]bool

TaskKnownNames returns the ${NAME} references that resolve cleanly for this candy: auto-exports ∪ candy.Vars keys.

func TaskSubstAutoExports added in v0.2026192.1709

func TaskSubstAutoExports(s string, img *buildkit.ResolvedBox) string

TaskSubstAutoExports substitutes the image-level auto-exports (USER/UID/GID/HOME) in s. ARCH/BUILD_ARCH are NOT substituted here.

func TaskSubstPath added in v0.2026192.1709

func TaskSubstPath(p string, img *buildkit.ResolvedBox) string

TaskSubstPath resolves a destination/path field: auto-export substitution plus leading ~/ expansion to the resolved HOME.

func TaskUnresolvedRefs added in v0.2026192.1709

func TaskUnresolvedRefs(s string, known map[string]bool) []string

TaskUnresolvedRefs returns the names of ${VAR} references in s not in known.

func TeardownHostDeploy added in v0.2026201.1934

func TeardownHostDeploy(paths *kit.LedgerPaths, rec *kit.DeployRecord, hostHome string, re kit.ReverseExecutor) error

TeardownHostDeploy reverses a single host/external deploy record: for each candy whose refcount drops to zero it replays the recorded ReverseOps, removes the env.d file, and deletes the candy record; then deletes the deploy record. Only RECORDED ops are replayed (record-and-replay). Shared by externalDeployTarget.Del (the local/external host-venue teardown).

func TopoSortByPopularity added in v0.2026193.1801

func TopoSortByPopularity(graph map[string][]string, popularity map[string]int) ([]string, error)

TopoSortByPopularity performs topological sort with popularity tie-breaking. Higher popularity candies come first among zero-in-degree candidates.

func TransferAndInstallPkgs added in v0.2026198.914

func TransferAndInstallPkgs(ctx context.Context, exec DeployExecutor, lp *LocalPkgDef, pkgFiles []string, opts EmitOpts) error

TransferAndInstallPkgs ships built package files onto a deploy target and installs them by rendering LocalPkgDef.InstallTemplate. It is venue-agnostic via the DeployExecutor: PutFile is a local filesystem copy for the host ShellExecutor and an scp for the SSHExecutor, and RunSystem is local sudo vs `ssh sudo`. One implementation serves BOTH the localpkg step (the local deploy target / the external vm deploy) AND the builder's install leg (BuilderStep.LocalPkg), so "ship packages to a venue and install them" has a single config-driven home (R3).

The staging dir is cleared before transfer so a re-run replaces stale content idempotently; the format's install command (e.g. `pacman -U`) is expected to be the upgrade form, so re-installing the same or a newer build never errors.

func TunnelConfigFromMetadata added in v0.2026202.1522

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 TunnelServiceFilename added in v0.2026194.615

func TunnelServiceFilename(boxName string) string

TunnelServiceFilename returns the systemd service filename for a cloudflare tunnel companion unit.

func UpdateBoxBase added in v0.2026193.1801

func UpdateBoxBase(imgName, parentName string, result map[string]*buildkit.ResolvedBox)

UpdateBoxBase updates an image's Base to point to the given parent.

func ValidateProvidesTemplate added in v0.2026198.358

func ValidateProvidesTemplate(tmpl string) bool

ValidateProvidesTemplate checks that only known placeholders are present. Allowed:

{{.ContainerName}}
{{.ContainerPort <N>}}   N must parse as a positive integer
{{.HostPort <N>}}        N must parse as a positive integer

func VenueHasPkgManager added in v0.2026198.914

func VenueHasPkgManager(ctx context.Context, exec DeployExecutor, lp *LocalPkgDef, opts EmitOpts) bool

VenueHasPkgManager probes the actual deploy venue for the package format's manager — the precondition for executing a LocalPkgInstallStep. The probe command comes from LocalPkgDef.Probe (e.g. `command -v pacman`), so this is config-driven, not a hardcoded pacman literal. Probing the VENUE (not the host running charly) is what makes the gate correct for a VM deploy: the guest may be a different distro than the operator host, and vice-versa. The executor is the venue (ShellExecutor → host, SSHExecutor → guest), so one probe through it is venue-accurate for both targets (R3). DryRun assumes true so the planner shows the build+install it WOULD do. A nil LocalPkgDef, empty probe, probe error, or non-matching venue returns false: charly never assumes a target can take a package.

func VerifyBindMounts added in v0.2026202.105

func VerifyBindMounts(mounts []ResolvedBindMount, boxName string) error

VerifyBindMounts checks that all bind mounts are ready to use: - Plain mounts: host directory must exist - Encrypted mounts: must be mounted (FUSE)

For encrypted mounts where FUSE is unmounted, an extra discrimination fires: if the cipher dir on disk holds real encrypted data (anything beyond the gocryptfs metadata files) AND the plain mount target is empty, we surface a louder error spelling out the data-loss risk. That's the immich-2026-04-incident shape — quadlet without ExecStartPre, FUSE never re-mounted after a reboot, container about to bind an empty plain/ over a populated cipher tree and start writing plaintext on top. The previous generic "not mounted" message was indistinguishable from a fresh-setup state where no harm exists yet.

func VmDeployEntryKeys added in v0.2026202.1522

func VmDeployEntryKeys(dc *BundleConfig, deployName string) []string

VmDeployEntryKeys resolves the per-host charly.yml bundle key(s) a VM teardown for deployName targets. It handles the case where a bundle's name differs from its `vm:<X>` runtime-state key: the literal-key delete + the `vm:`-form From-scan below remain for the DIRECT `charly vm destroy <entity>` path and any legacy `vm:<name>` teardown key: they target the literal deployName key AND — when deployName is "vm:<X>" — every bundle whose `vm:` cross-ref names <X>. Because domain identities are unique and never equal an entity a sibling shares, the From-scan can no longer over-match sibling beds during a deploy teardown.

func WaitForContainerReady added in v0.2026202.1112

func WaitForContainerReady(bed string)

WaitForContainerReady gates on the container being exec-able AND its supervisord-managed children having left their transitional states, so a one-shot check-live port/service probe never races a child that has not yet bound. `charly start` returns when systemd reports the service active, but supervisord's autostart children are still STARTING for a moment after. This polls `supervisorctl status` until no child is STARTING/BACKOFF (a child binds its port the instant it reaches RUNNING) instead of sleeping a fixed, host-tuned interval. Images without supervisord settle immediately. Best-effort: silent on timeout (the next check-live step surfaces the real failure). Reads the project's readiness bounds via kit.ReadinessProvider() — the SAME plugin-portable channel every other executor wait uses (the host threads its resolved bounds via ResolvedReadiness.PluginEnv; a project-unaware caller falls back to kit's built-in defaults).

func WaitForVmSshReady added in v0.2026202.1112

func WaitForVmSshReady(domainID string)

WaitForVmSshReady gates on the VM being SSH-reachable AND cloud-init having settled, using the SAME deterministic SSHExecutor preflight the VM check-live path and the external vm deploy walk run — NOT a fixed sleep. WaitForSSH polls until sshd answers; WaitForCloudInit retries until an ssh connection survives a `cloud-init status` poll (the deterministic cloud-init-settled signal — so deploy-add never races a still-running first-boot pacman). domainID is the per-deploy DOMAIN IDENTITY (the bed/member deploy name), not the shared kind:vm entity — the alias the create path published. Best-effort: silent on timeout (the downstream deploy-add surfaces the real error).

func WalkDeploymentTree added in v0.2026190.848

func WalkDeploymentTree(rootPath string, root *BundleNode, parentExec DeployExecutor, visit DeployTreeVisitor) error

WalkDeploymentTree performs a pre-order walk rooted at the given node, calling visit on each node. Dotted-path accumulation is handled internally: the root's `rootPath` argument seeds the identifier; children are rendered as `<parent>.<childKey>`.

Errors short-circuit: as soon as any visit call returns a non-nil error, the walk stops and that error propagates.

Types

type AliasYAML added in v0.2026190.848

type AliasYAML = spec.AliasYAML

type ApkInstallStep

type ApkInstallStep = spec.ApkInstallStep

type ApkPackageSpec

type ApkPackageSpec = spec.ApkPackageSpec

type ArtifactRef

type ArtifactRef = spec.ArtifactRef

type BuilderDef

type BuilderDef = spec.BuilderDef

type BuilderPreresolved added in v0.2026190.848

type BuilderPreresolved struct {
	Context map[string]any
	Reverse []ReverseOp
}

BuilderPreresolved is one candy×builder's pre-resolved payload. The Candy-coupled functions that BUILD it (FLOOR-SLIM-proper Unit-8: candy/plugin-bundle's own preresolveBuilderContexts, over exec.InvokeProvider — the CONNECT step alone stays charly-core, which owns loadProjectPlugins/ScanAllCandyWithConfigOpts) build it; the compiler only reads it. The externalized-builder WORD SET itself needs no new sharing mechanism — it already rides the wire as spec.ResolvedProject.ExternalizedBuilders (the resolved-project envelope every "resolved-project" HostBuild caller, incl. candy/plugin-bundle's compile.go, already re-hydrates).

type BuilderRunOpts added in v0.2026190.848

type BuilderRunOpts = spec.BuilderRunOpts

type BuilderStep

type BuilderStep = spec.BuilderStep

type BundleConfig added in v0.2026190.848

type BundleConfig struct {
	Provides *spec.ProvidesConfig  `yaml:"provides,omitempty" json:"provides,omitempty"`
	Bundle   map[string]BundleNode `yaml:"deploy" json:"deploy"`
	// Sidecar carries the project's sidecar-template library as OPAQUE bodies
	// (the raw PluginKinds["sidecar"] map). candy/plugin-sidecar's OpResolve merges
	// these UNDER each deploy node's own overrides; the kernel reads no fields
	// (the sidecar de-type, Cutover D).
	Sidecar map[string]json.RawMessage `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

--- BundleConfig state container (moved from charly/deploy.go, P4) --- BundleConfig represents per-machine deployment overrides (~/.config/charly/charly.yml). Only runtime/deployment fields are supported — build-time fields are structurally excluded.

Schema v4: the top-level map key is `deployment:` (singular, flat). The legacy `images:` / `deployments.images.*` nesting is gone — all target kinds (host / vm / pod / k8s) live under the single `deployment:` map.

func ExportAllBox added in v0.2026196.928

func ExportAllBox(rp *spec.ResolvedProject) *BundleConfig

ExportAllBox exports all runtime-relevant fields for all enabled boxes in a RESOLVED project envelope. Relocated from charly/deploy.go (K5-Unit-1, the #67 keystone): the former `ExportAllBox(cfg *Config)` read the live *Config graph; this rewrite reads the spec.ResolvedProject envelope the "resolved-project" HostBuild seam (K5-Unit-0) produces, so the deploy state model is built from the SAME envelope inspect/list/validate consume — no core *Config dep. The box-authored deploy-overlay surfaces (description / env / env_file / security / network) ride #ResolvedBoxView (grown in this cutover); version is the box's EffectiveVersion (the stable content-derived CalVer), matching the prior behaviour's `img.Version`.

func LoadBundleConfig added in v0.2026196.928

func LoadBundleConfig() (*BundleConfig, error)

LoadBundleConfig reads the per-host deploy overlay (~/.config/charly/charly.yml) through the unified loader — the SAME LoadUnified path as every project charly.yml. Returns nil, nil if the file doesn't exist. Relocated from charly/deploy.go (K5-Unit-1); the LoadUnified hop reaches core through DeployStateHost.LoadUnifiedBundleConfig.

Every transform the old bespoke parser did — the `images:` legacy-key reject, the deployment-tree / required-box: / preemptible / ephemeral-naming validation, and the ephemeral→disposable auto-promotion — runs INSIDE LoadUnified (its version gate + the deploy-validation block subsume the legacy check; the ephemeral/naming validators + promotion were consolidated there so a PROJECT charly.yml's inline deploy: entries get them too — R3, one path).

func LoadBundleConfigViaSeam added in v0.2026203.824

func LoadBundleConfigViaSeam(ctx context.Context, ex *sdk.Executor, caller string) (*BundleConfig, error)

LoadBundleConfigViaSeam reads the per-host deploy overlay (~/.config/charly/charly.yml) via the "pod-config-load-bundle" HostBuild seam — placement-invariant: works identically whether the calling plugin is compiled-in or out-of-process, unlike LoadBundleConfig (deploy_file.go), which silently no-ops outside the process that ran charly-core's own init(). ex is the caller's already-resolved reverse-channel executor (a package-var stash for a single-command-per-process command plugin, or a per-call sdk.ExecutorForInvoke result for a multi-call verb provider) — this function does not resolve one itself, since HOW a caller obtains its executor is its own placement concern, not this seam's. caller is a short human-readable label (e.g. "candy/plugin-status nested tree") threaded into the wire request for host-side diagnostics, mirroring each call site's former Caller string. Returns (nil, nil) on an absent/empty overlay, matching LoadBundleConfig's own contract.

func LoadDeployConfigForRead added in v0.2026196.928

func LoadDeployConfigForRead(context string) *BundleConfig

LoadDeployConfigForRead loads charly.yml for read-only consumption. Unlike the historical `dc, _ := LoadBundleConfig()` pattern (silently discards validation errors → caller proceeds with nil → feature degrades invisibly), this helper SURFACES the load error as a stderr warning while still returning nil — preserving the existing caller nil-check contract but giving the operator visibility into why a command behaved as if charly.yml were absent. Relocated from charly/deploy.go (K5-Unit-1).

context is a short human-readable label included in the warning message so the operator can trace which code path noticed the problem (e.g. "charly status", "config injectEnvProvides").

func LoadDeployConfigForWrite added in v0.2026196.928

func LoadDeployConfigForWrite(context string) (*BundleConfig, error)

LoadDeployConfigForWrite loads charly.yml for mutation. Unlike the historical `dc, _ := LoadBundleConfig()` pattern (silently discards validation errors → writer constructs an empty config → SaveBundleConfig truncates the file), this helper PROPAGATES the load error so writers can ABORT instead of destroying data. Relocated from charly/deploy.go (K5-Unit-1).

context is a short human-readable label included in the error message (e.g. "saveDeployState"). Returns (nil, error) when the file exists but failed parse/validation; (fresh empty config, nil) when the file doesn't exist; (parsed config, nil) on clean load.

func LoadDeployFile added in v0.2026193.1222

func LoadDeployFile(path string) (*BundleConfig, error)

LoadDeployFile reads a charly.yml from an arbitrary path into a BundleConfig.

func MergeDeployConfigs added in v0.2026190.848

func MergeDeployConfigs(configs ...*BundleConfig) *BundleConfig

MergeDeployConfigs merges multiple DeployConfigs left-to-right. Later configs take precedence (field-level replace per image). The merge walks every yaml-tagged field of BundleNode via reflect: a field copies from src → dst when src's value is non-zero (string != "", slice/map/ptr not nil, bool != false, numeric != 0). This makes adding a new field to BundleNode automatically merge-correct — the pre-2026-05 hand-rolled per-field merge silently dropped 19+ fields (ResolvedPort, Description, Secret, Sidecar, Shell, Kubernetes, ForwardGpgAgent, ForwardSshAgent, Kind, Replica, Restart, Schedule, Resources, Expose, Storage, Probes, Cpus, Ram, DiskSize) whenever any merge → save cycle ran.

The yaml tag `-` (currently only BundleNode.Inside, a derived runtime field) skips the merge. Untagged fields are also skipped.

func ProjectBundleConfig added in v0.2026210.828

func ProjectBundleConfig(uf *spec.UnifiedFile) *BundleConfig

ProjectBundleConfig returns the *BundleConfig equivalent (the deployments: section of the authored file, independent of any per-machine ~/.config/charly/charly.yml, which remains loaded separately by LoadBundleConfig). nil when the file carries no deploy/provides/sidecar content.

func (*BundleConfig) DeployedContainerNames added in v0.2026190.848

func (dc *BundleConfig) DeployedContainerNames() []string

DeployedContainerNames returns the sorted, de-duplicated container names for every bundle entry.

func (*BundleConfig) GlobalEnvForImage added in v0.2026190.848

func (dc *BundleConfig) GlobalEnvForImage(consumerKey, ctrName string, acceptedEnv map[string]bool) []string

GlobalEnvForImage builds the env-var injection list for a consumer container from the deploy state's env + MCP provides, filtered by the consumer's accepts.

func (*BundleConfig) Lookup added in v0.2026190.848

func (dc *BundleConfig) Lookup(deployName, instance string) (BundleNode, bool)

Lookup returns the BundleNode for (deployName, instance), or (zero, false) when the entry is absent. Safe to call on a nil *BundleConfig — lets callers chain `loadDeployConfigForRead(...).Lookup(deployName, instance)` without a separate nil check. deployName is the charly.yml key base the caller is operating on (typically c.Image), NOT the baked image short-name — for a kind:check bed or Pattern-B deploy the two differ. Pass the deploy key, never a value derived from an image label (see MergeDeployOntoMetadata).

func (*BundleConfig) LookupKey added in v0.2026190.848

func (dc *BundleConfig) LookupKey(key string) (BundleNode, bool)

LookupKey looks up a deploy entry by its full charly.yml key (e.g. "foo", "foo/instance", "vm:name"). Safe on nil receiver.

func (*BundleConfig) OccupiedHostPorts added in v0.2026190.848

func (dc *BundleConfig) OccupiedHostPorts(excludeKey string) map[int]bool

OccupiedHostPorts returns the set of host ports already claimed by bundle entries other than excludeKey (resolved pins preferred over authored).

type BundleNode added in v0.2026190.848

type BundleNode = spec.BundleNode

func FindBundleNode added in v0.2026202.105

func FindBundleNode(bundle map[string]BundleNode, name string) *BundleNode

FindBundleNode locates a deploy node by key across the WHOLE tree — every top-level root, plus its nested Children and peer Members, at any depth. Returns nil when no node with that key exists anywhere in the tree. The SDK-side twin of ResolveNodePath (which descends a known DOTTED path) and FindVmDeployNode (which searches only the top level) — this one searches for an unqualified NAME anywhere, needed when the caller only has the node's bare key, not its path from a root.

Moved from charly/k3s_post.go (Cutover B unit 5, P13-KERNEL-B): a pure BundleNode-tree search with zero loader/registry dependency — the scoping-map re-audit found this was the ONLY genuinely movable piece of its family; the LoadUnified-coupled orchestration around it (resolving which deploy owns a VM entity, reading the persisted port-forward allocation) stays host-side, since LoadUnified/materialize is K1-permanent core (R-E2).

func FindVmDeployNode added in v0.2026190.848

func FindVmDeployNode(deploys map[string]BundleNode, name, vmName string) (BundleNode, bool, error)

FindVmDeployNode finds the BundleNode for a vm-target deploy. It is THE shared "which deploy entry backs this VM" lookup used by both `charly bundle add` (artifact-env collection) and `charly check live` (tests overlay), so the two never diverge. Resolution order:

  1. by deploy NAME (the entry key) — the precise match;
  2. by the legacy "vm:<name>" key form;
  3. by scanning for any target:vm entry whose `vm:` field == vmName (or == name) — the fallback when the caller only knows the vm entity.

Keying by the deploy NAME first is load-bearing: a bed whose key differs from its vm entity (e.g. check-k3s-vm -> vm: k3s-vm) is found by its key, not mis-resolved via the vm entity name.

RCA #14 (FINAL/K5 unit 6a): the step-3 fallback scan has no uniqueness guarantee — with N top-level vm deploys sharing one base template/entity (a common shape: several disposable eval beds all `from: eval-vm`), a first-wins match is genuinely AMBIGUOUS and, because Go randomizes map iteration order per process, can silently return a DIFFERENT (unrelated) deploy's node on different runs of the identical lookup — proven live: `check-substrate` and `check-builder-vm` (both real top-level vm deploys, so their own descendants' hoisted plan steps land on THEIR OWN Plan) vs `check-group-vm`/`check-structkind-vm` (both promoted from a peer Member, so their Plan was already relocated onto their FORMER parent's root before promotion, leaving it empty) all shared `from: eval-vm` — a caller resolving an unrelated vm entity could nondeterministically inherit check-substrate's own hoisted plan steps, and their embedded venue then points at check-substrate's own domain. err is non-nil ONLY when the step-3 fallback scan finds 2+ candidates — steps 1-2 are exact-key matches and never ambiguous.

func MergeBundleNode added in v0.2026190.848

func MergeBundleNode(dst, src BundleNode) BundleNode

MergeBundleNode applies non-zero fields from `src` onto `dst` and returns the merged copy. Walks every yaml-tagged field via reflect; the yaml `-` tag (derived/runtime-only fields) is skipped. Same precedence rule as the underlying merge: src non-zero wins, otherwise dst passes through. Per R3 the single helper replaces the hand-rolled per-field merges that previously lived in MergeDeployConfigs (drift-prone — every new struct field needed a remembered append, and 19+ were missed).

type CacheMountDef

type CacheMountDef = spec.CacheMount

type CacheMountSpec

type CacheMountSpec = spec.CacheMountSpec

type CandyArtifact added in v0.2026190.848

type CandyArtifact = vmshared.CandyArtifact

type CandyCapabilities added in v0.2026190.848

type CandyCapabilities = spec.CandyCapabilities

type CandyModel added in v0.2026190.848

type CandyModel = spec.CandyReader

candy_model.go — CandyModel is now promoted to spec.CandyReader (W9: the mass-edit interface relocation). spec is the shared contract home an import-clean charly file can reach WITHOUT an sdk mechanism-kit import (mirrors the loader_seam.go DocParser/ProjectWalker precedent + the PackageSection/TagPkgConfig/RouteConfig/ApkPackageSpec/ServiceEntry/HooksConfig/SecurityConfig promotions already sitting in layer_model.go/steps.go/candy_field_aliases.go). This alias keeps every existing deploykit-internal + charly `deploykit.CandyModel` reference compiling unchanged; new call sites in an import-clean file should reach spec.CandyReader directly.

func NewSpecCandyModel added in v0.2026195.544

func NewSpecCandyModel(m spec.CandyModel, v spec.CandyView) CandyModel

NewSpecCandyModel builds a deploykit.CandyModel from a candy's resolved-project envelope views. m + v are the CandyModels[name] / Candies[name] entries; SourceDir comes from m.

type CandyPluginDecl added in v0.2026190.848

type CandyPluginDecl = vmshared.CandyPluginDecl

type CandyRef added in v0.2026190.848

type CandyRef = spec.CandyRefEntry

candy_ref.go — CandyRef is now promoted to spec.CandyRefEntry (W9: the mass-edit interface relocation, alongside CandyModel → spec.CandyReader). spec.CandyRef already names the DISTINCT CUE-authored wire form (a bare string); CandyRefEntry is the runtime model with derived .Bare()/.Version()/.IsRemote() methods — still reachable through this alias unchanged. The pure ref-string helpers below (BareRef/StripVersion/IsRemoteCandyRef/ToCandyRefs) keep their established names and signatures (candy/plugin-box + several charly files call them by these names) — forwarding to the promoted spec functions so the logic has exactly one home.

func ToCandyRefs added in v0.2026190.848

func ToCandyRefs(raw []string) []CandyRef

ToCandyRefs wraps raw ref strings into CandyRef values. Returns nil for a nil/empty input so an absent list stays absent.

type CandyYAML added in v0.2026190.848

type CandyYAML = spec.Candy

type CollectedSecret added in v0.2026194.615

type CollectedSecret struct {
	Name           string // podman secret name: "charly-<image>-<name>"
	Target         string // container mount path
	Env            string // env var name INSIDE the container
	HostEnv        string // env var name on the HOST to read the value from
	SecretName     string // original secret name from the candy manifest
	Service        string // credential store service override
	Key            string // credential store key override
	RotateOnConfig bool   // bypass PodmanSecretExists short-circuit (rotate every config)
}

CollectedSecret is a fully resolved secret ready for provisioning + quadlet emission (the Secret= directive). Resolved runtime state (ruling C: the plugin builds it internally), not a wire type — owned here as a plain struct.

func ApplySecretRefresh added in v0.2026202.105

func ApplySecretRefresh(secrets []CollectedSecret, refresh []string) ([]CollectedSecret, []string)

ApplySecretRefresh marks the named secrets (matched by their manifest SecretName; the literal "all" matches every secret) RotateOnConfig for THIS provisioning run, so ProvisionPodmanSecrets removes and recreates their podman secrets — the charly-native replacement for the retired ad-hoc `podman secret rm` re-provisioning path. Returns the requested names that matched nothing so the caller can surface typos. NOTE: a candy-owned auto-generated secret gets a NEW random value on refresh; services that persisted the old value (an initialized database) must be re-initialized by the operator.

func CollectSecretsFromLabels added in v0.2026202.105

func CollectSecretsFromLabels(boxName string, labelSecrets []spec.LabelSecretEntry) []CollectedSecret

CollectSecretsFromLabels reconstructs secrets from image label metadata.

func ProvisionPodmanSecrets added in v0.2026202.1112

func ProvisionPodmanSecrets(engine, boxName, instance string, secrets []CollectedSecret, autoGenerate bool, credServiceVNC string, cred CredentialAccess) (provisioned []CollectedSecret, fallbackEnv []string, err error)

ProvisionPodmanSecrets creates podman secrets from the credential store. Returns the secrets that were successfully provisioned and any that fell back to env vars.

type CredentialAccess added in v0.2026202.1112

type CredentialAccess struct {
	Resolve CredentialResolver
	Write   CredentialWriter
}

CredentialAccess bundles the two credential-store operations this file's orchestration needs. Both fields are required by ProvisionPodmanSecrets/GenerateAndStoreSecret; only Resolve is used by ResolveSecretValue/CollectCandySecretAccepts/ResolveHookSecretEnv.

type CredentialResolver added in v0.2026202.1112

type CredentialResolver func(envVar, service, key, defaultVal string) (value, source string)

CredentialResolver abstracts a single credential-store lookup — the shape of charly-core's ResolveCredential(envVar, service, key, defaultVal) (value, source).

type CredentialWriter added in v0.2026202.1112

type CredentialWriter func(service, key, value string) error

CredentialWriter abstracts a single credential-store persist — the shape of charly-core's CredentialStore.Set(service, key, value) error.

type CycleError added in v0.2026192.1709

type CycleError struct {
	Cycle []string
}

CycleError represents a circular dependency error

func (*CycleError) Error added in v0.2026192.1709

func (e *CycleError) Error() string

type DataProvisionMode added in v0.2026198.345

type DataProvisionMode string

DataProvisionMode controls how ProvisionData copies files.

const (
	// DataProvisionInitial copies all data into empty directories (cp -a).
	// Used by charly config for first-time setup.
	DataProvisionInitial DataProvisionMode = "initial"

	// DataProvisionMerge adds missing files without overwriting existing ones (cp -an).
	// Used by charly update to add new data while preserving user modifications.
	DataProvisionMerge DataProvisionMode = "merge"

	// DataProvisionForce overwrites all data unconditionally (cp -a).
	// Used by charly config --force-seed.
	DataProvisionForce DataProvisionMode = "force"
)

type DataYAML added in v0.2026190.848

type DataYAML = spec.DataYAML

type DeployExecutor added in v0.2026190.848

type DeployExecutor = spec.DeployExecutor

func AppendHopForFlatPath added in v0.2026190.848

func AppendHopForFlatPath(chain DeployExecutor, node *BundleNode, flatPath, leaf string) (DeployExecutor, error)

AppendHopForFlatPath stacks one executor hop so commands land inside `node`'s substrate. flatPath is the dotted path with dots replaced by underscores — the host-side container name suffix; leaf is the final path segment (the node's own key), used for a pod deployed STANDALONE inside a VM guest (which has no parent-path concept — see the pod case).

func CheckBoxContainerChain added in v0.2026197.1320

func CheckBoxContainerChain(engine, imageRef string) (DeployExecutor, func(), error)

CheckBoxContainerChain is the R44 Option-A box-mode executor: instead of a fresh `podman run --rm <img> bash` per check step (N container SETUPS — each a rootfs mount + temp-/etc/passwd generation that races concurrent build-store churn, the exit-127 passwd-file-race root cause), it creates ONE long-lived container from imageRef and returns a ContainerChain exec'ing into it (O(N)→O(1) setups), unifying box-mode with check-live's `podman exec` model. The per-step isolation of `--rm` is intentionally traded for the shared container: `check:` steps are idempotent goss-style probes and check-live ALREADY runs them against ONE shared container, so box-mode merely aligns (see the check skill; any check secretly relying on fresh-container isolation is flushed by the all-images gate).

The single container-CREATE is the one remaining setup that can race the store; its failure is CLASSIFIED (kit.ClassifyContainerInfraFailure): an infra failure returns a MARKED error so the caller exits the INFRA class (a plain error → exit 1), never checks-failed — a residual setup failure is surfaced LOUDLY, never absorbed by a retry (classify-only, R44 ruling).

Returns the exec chain + a teardown closure (always safe to call, even on the error paths).

func ContainerChain added in v0.2026190.848

func ContainerChain(engine, containerName string) DeployExecutor

ContainerChain returns a one-hop chain that exec's into a single named running container (`<engine> exec <name> bash`). Convenience for the simple `charly check live <name>` path where there is no nested dotted path to walk — equivalent to ResolveDeployChain on a single-segment dotted path that resolves to a pod node, but skips the tree lookup.

Delegates to kit.ContainerChainFromDescriptor (K1-unblock W3 Unit B, R3 — one construction, not two): that function is the SAME shape, promoted to sdk/kit so kit.VenueFromDescriptor's new "container" VenueDescriptor kind re-materializes byte-identically to this constructor, letting a plugin-constructed ContainerChain venue round-trip over InvokeProvider's VenueDescriptor seam.

func RootExecutorForDeployNode added in v0.2026190.848

func RootExecutorForDeployNode(node *BundleNode) (DeployExecutor, error)

RootExecutorForDeployNode selects the ROOT DeployExecutor for a `target: local` deployment node from its `host:` field — the single source of truth for "where does a local deploy's work run?", shared by `charly bundle add` (the local deploy target.Add) and `charly check live` (runLocalCheck) so neither re-implements the selection (R3):

host: ""  / "local"        → ShellExecutor{} (this machine, direct shell)
host: "<user>@<machine>"   → &SSHExecutor{User, Host, Port, …}
host: "<machine>" + user:  → SSHExecutor with User from node.User

It does NOT handle the nested-inside-a-parent case (opts.ParentExec); that stays in the local deploy target.Add because it's deploy-execution-specific. Returns ShellExecutor{} for a nil node.

func VmChildExecutor added in v0.2026190.848

func VmChildExecutor(parentExec DeployExecutor, deployName string) (DeployExecutor, error)

VmChildExecutor wraps parentExec with an SSH jump into the VM represented by this node. At the root (parentExec == nil or ShellExecutor), the child gets a plain SSHExecutor — no nesting overhead for the common case of a VM on localhost.

The SSH alias keys off the per-deploy DOMAIN IDENTITY (charly-<VmDomainIdentity(deployName)>), NOT node.From (the shared kind:vm entity) — `charly vm create <entity> --domain <deploy>` writes the managed stanza under `charly-<deploy>` (P33). Several beds may share one entity via `from:`, so an entity-keyed alias collides them on ONE stanza (the R10 defect where sibling beds both derived `charly-eval-vm`); keying by the deploy makes the alias distinct per bed and matches the stanza vm create actually wrote. A direct create (deploy == entity) resolves to `charly-<entity>` naturally, and VmDomainIdentity flattens a dotted member path consistently with the domain the lifecycle named (bundle_members.go's `vmDomainIdentity(memberKey)`).

type DeployTarget added in v0.2026190.848

type DeployTarget = spec.DeployTarget

type DeployTreePhase added in v0.2026190.848

type DeployTreePhase int

DeployTreePhase indicates which lifecycle phase the walker is in. Pre-order for add; teardown walks the flat-path chain (deploy_chain.go).

const (
	DeployTreePhaseAdd DeployTreePhase = iota
	DeployTreePhaseDel
)

type DeployTreeVisitor added in v0.2026190.848

type DeployTreeVisitor func(path string, node *BundleNode, parentExec DeployExecutor) (childExec DeployExecutor, err error)

DeployTreeVisitor is invoked once per node in the walk. It receives the node's dotted path, the node itself, and the parent executor (nil at the root). The return value is the DeployExecutor that this node's CHILDREN should use as their parent. A host-target node returns the same executor it was given (candies applied in-place on the parent venue); a container or vm node returns a NestedExecutor that drills into the newly-created environment.

Returning (nil, nil) for a node with children is an error — it means "cannot compute child executor", which the walker surfaces with the offending path.

type EmitOpts added in v0.2026190.848

type EmitOpts = spec.EmitOpts

func InstallOptsApplyTo added in v0.2026190.848

func InstallOptsApplyTo(o *InstallOptsConfig, opts EmitOpts) EmitOpts

ApplyTo merges install_opts settings into an EmitOpts. CLI flags still win — charly.yml provides defaults, not overrides. Nil receiver is a no-op.

type EnsureBuildersParams added in v0.2026195.1912

type EnsureBuildersParams struct {
	Dir   string   `json:"dir"`
	Words []string `json:"words"`
}

EnsureBuildersParams carries the builder words to core ensureBuildersConnected.

type EnvConfig added in v0.2026190.848

type EnvConfig = kit.EnvConfig

type EnvDependency added in v0.2026190.848

type EnvDependency = vmshared.EnvDependency

type ExternalPluginStep

type ExternalPluginStep = spec.ExternalPluginStep

type ExternalStep added in v0.2026190.848

type ExternalStep = spec.ExternalStep

type ExtractYAML added in v0.2026190.848

type ExtractYAML = spec.ExtractYAML

type FileStep

type FileStep = spec.FileStep

type Gate

type Gate = spec.Gate

type Generator added in v0.2026192.1709

type Generator struct {
	Dir            string
	Candies        map[string]CandyModel
	Tag            string
	Boxes          map[string]*buildkit.ResolvedBox
	BuildDir       string
	Containerfiles map[string]string // cached content per image (build pipes via stdin)
	GlobalOrder    []string          // popularity-weighted global candy order for cache reuse
	RequestedBoxes []string          // scopes which Containerfiles Generate() writes; empty = all

	// DevLocalPkg makes localpkg candies (the charly toolchain) build from LOCAL
	// in-development source instead of the published release — set ONLY for
	// disposable check-bed image builds. See RenderLocalPkgImageInstall (localpkg.go).
	DevLocalPkg bool

	// Config + InitConfig are the RESOLVE-side inputs the host render-prep pass
	// (RenderPrepBox/RenderPrepAll in render_prep.go, K3-U3) reads to fill the
	// per-box build-render caches (RenderCandyOrder/CandyCaps/ActiveInits/
	// InitSystem/InitDef/BakedMetadata) before the resolved-project envelope is
	// projected. The host (charly's toDeploykit) sets both from its resolved state;
	// the plugin-build render path (NewRenderGeneratorFromProject) leaves them nil
	// and reads the pre-computed caches from the envelope instead, so it never
	// invokes render-prep.
	Config     *spec.Config
	InitConfig *buildkit.InitConfig

	// EmitPluginOp dispatches a non-"command" plugin verb op through the core
	// Provider registry: a ProvisionActor returns (script, true, nil) so emitTasks
	// emits it via EmitCmd; any other provider returns (fragment, false, nil) so
	// emitTasks writes it verbatim. The Provider registry + OpEmit dispatch (and its
	// error strings) STAY CORE; charly's toDeploykit() wires this closure. Op =
	// vmshared.Op = spec.Op.
	EmitPluginOp func(op *spec.Op, img *buildkit.ResolvedBox) (out string, isScript bool, err error)

	// CollectBoxPorts returns an image's aggregated exposed ports. The host wires this
	// seam to deploykit.CollectBoxPorts(cfg, layers, boxName) (ports_collect.go — the
	// aggregator itself now lives in this package; the host supplies the resolved
	// spec.Config + Candy graph it still holds RESOLVE-side). The seam remains so the
	// render generator need not carry the resolved config; it collapses when the render
	// reads the resolved-project envelope directly.
	CollectBoxPorts func(boxName string) ([]string, error)

	// ValidateEgress gates a hand-built config (traefik routes, …) against the
	// egress CUE schemas before it is written into the build context. Wraps core
	// ValidateEgress (the egress validation stays core / its plugin) — mode "bytes".
	ValidateEgress func(kind, label string, data []byte) error

	// ValidateTextEgress gates a rendered TEXT artifact (the Containerfile — rejects the
	// "<no value>" template-failure marker) against the rendered_text constraint. Wraps core
	// validateTextEgress (kind "rendered_text", mode "text"). Distinct from ValidateEgress (bytes)
	// because the Containerfile is a rendered string, not a YAML/JSON blob (#67 render-DRIVE move).
	ValidateTextEgress func(label, text string) error

	// RenderService materializes a ServiceEntry into a RenderedService via
	// candy/plugin-init's OpResolve (the init-system template knowledge lives in
	// the plugin) and egress-gates the unit text. The plugin Invoke + the egress
	// gate STAY CORE; charly's toDeploykit() wires core RenderService here. Used by
	// GenerateInitFragments (fragment_assembly model).
	RenderService func(entry *spec.ServiceEntry, def *spec.ResolvedInit, ctx spec.ServiceRenderContext) (*spec.RenderedService, error)

	// RewriteHeaderCopyForRemote rewrites a stage_header_copy COPY line so its
	// source points at a materialized remote build-config asset (host fs write).
	// Wraps core rewriteHeaderCopyForRemote → materializeBuildConfigAsset (host
	// fs, stays core). Used by EmitInitFragmentStages.
	RewriteHeaderCopyForRemote func(headerCopy string) (string, error)

	// ExternalizedBuilders is the set of builder words whose inline render crosses
	// to a plugin's OpResolve (vs the embedded builder: vocabulary install_template).
	// Wired from core's externalizedBuilders registry fact. Read by WriteCandySteps
	// to pick the inline-builder branch.
	ExternalizedBuilders map[string]bool

	// RenderLocalPkgImageInstall renders a candy's localpkg OS-package install for
	// the image build. The PRODUCTION path is pure render (a curl+install RUN); the
	// DevLocalPkg path builds the in-development package on the HOST (makepkg). This
	// is a PURE sdk/deploykit function now (W3, deploykit.RenderLocalPkgImageInstall
	// in localpkg.go) — NOT a host-coupled seam like its siblings above; the bare
	// constructor wires it directly (no core closure needed). Used by WriteCandySteps.
	RenderLocalPkgImageInstall func(step *LocalPkgInstallStep, devLocalPkg bool, imageDir, boxName string) (string, error)

	// ResolveInlineBuilder connects + OpResolves an externalized INLINE builder and
	// returns its in-candy fragment (C10 InlineFragment). Wraps the core builder-emit
	// cluster (ensureBuildersConnected + registry ResolveBuilder + resolveBuilderStage;
	// registry-coupled, stays core), preserving its per-failure error strings. Used by
	// WriteCandySteps.
	ResolveInlineBuilder func(candyName, builderName string, bDef *buildkit.BuilderDef, ctx *spec.BuildStageContext, img *buildkit.ResolvedBox) (inlineFragment string, err error)

	// EnsureBuildersConnected connects the EXTERNALIZED detection-builder plugins an
	// image triggers (on-demand, scoped — the SAME machinery the deploy build PRE-PASS
	// uses, R3), so their OpResolve build leg can be Invoked. Wraps core
	// ensureBuildersConnected(ctx, cfg, dir, detected) (registry-coupled, stays core).
	// Used by EmitBuilderStages.
	EnsureBuildersConnected func(detected []string) error

	// ResolveDetectionBuilderStage resolves + Invokes an externalized DETECTION-builder
	// plugin's OpResolve build leg for one (candy, builder), returning the rendered
	// BuilderResolveReply. deploykit builds the render context (BuildStageContext +
	// builderResolveInputFrom) and passes the serializable input; the seam does the
	// registry ResolveBuilder + OpResolve Invoke (its "not connected" error preserved
	// byte-exact). Wraps core resolveDetectionBuilder's registry half. Used by EmitBuilderStages.
	ResolveDetectionBuilderStage func(builderName string, in spec.BuilderResolveInput, img *buildkit.ResolvedBox) (spec.BuilderResolveReply, error)

	// ResolveExternalBuilderStage resolves + Invokes an `external_builder:`-selected
	// out-of-tree builder provider's OpResolve, returning the reply (non-empty Stage
	// required). The seam does registry ResolveBuilder + the *grpcProvider assertion +
	// the minimal-input Invoke (all its error strings preserved byte-exact). Wraps core
	// resolveExternalBuilder + its provider resolution. Used by EmitExternalBuilderStages.
	ResolveExternalBuilderStage func(word, candyName string, img *buildkit.ResolvedBox) (spec.BuilderResolveReply, error)

	// EmitBakedPlugins bakes each composing candy's `bake_plugin:` out-of-tree plugin
	// binaries into the FINAL image at /usr/lib/charly/plugins/. The deploykit render
	// calls this seam post-main-FROM (after EmitExternalBuilderArtifacts). The host
	// (live path) wires the charly emitBakedPlugins closure; plugin-build wires it via
	// HostBuild("bake-plugins"). Used by generateContainerfile (#67 render-DRIVE move).
	EmitBakedPlugins func(b *strings.Builder, boxName string, candyOrder []string) error

	// CollectBoxVolume returns an image's aggregated volume mounts. Wraps the core
	// CollectBoxVolume(cfg, layers, boxName, home, nil) aggregator (which reads the core
	// Config + Candy graph — RESOLVE-side, stays core). Used by generateDataImageContainerfile.
	CollectBoxVolume func(boxName string, home string) ([]VolumeMount, error)
	// contains filtered or unexported fields
}

Generator is the build-mode RENDER engine, homed in sdk/deploykit (P8).

WHY deploykit and not buildkit: the render calls the build-compile helpers resolveCascadePackages / compileLocalPkgStep / compileShellSnippetSteps (sdk/deploykit/install_build.go) and reads deploykit.CandyModel. The import DAG is one-way deploykit→buildkit, so a buildkit-hosted render would cycle. deploykit already holds the build-compile foundation, so the render belongs here; buildkit stays the low keystone layer (ResolvedBox, RenderTemplate).

The render operates over CandyModel (never the charly-side *Candy) and over buildkit.ResolvedBox. Every host-coupled dependency the render needs — the Config/Candy-graph label aggregators (CollectBox*/Collect*/Aggregate*), init resolution (ActiveInit/ResolveInitSystem), the registry/plugin Invoke legs (ensureBuildersConnected, OpResolve/OpEmit), the registry image probe (InspectImageUser), and the few Config.Defaults/BoxConfig reads — is threaded in as a function-value SEAM field (added compiler-driven as each render method is relocated onto this type). The charly-side resolve half (NewGenerator: LoadConfig/ScanAllCandy/loadProjectPlugins/ResolveAllBox/ ComputeIntermediates/GlobalCandyOrder) constructs this value and wires the seams from the existing core functions; the podman orchestration (runBoxBuild/runBoxGenerate/push/retry/manifest) lives in candy/plugin-build and drives the build, calling back HostBuild for this host-side render.

func NewRenderGenerator added in v0.2026192.1709

func NewRenderGenerator() *Generator

NewRenderGenerator constructs a render Generator with its unexported per-image reply caches initialized. The caller (charly's g.toDeploykit()) sets the exported data fields + seam function values directly on the returned value — the caches cannot be set cross-package, which is the whole reason this constructor exists.

func NewRenderGeneratorFromProject added in v0.2026196.1550

func NewRenderGeneratorFromProject(ctx context.Context, ex *sdk.Executor, rp *spec.ResolvedProject, dir string, devLocalPkg bool) (*Generator, error)

NewRenderGeneratorFromProject constructs a deploykit.Generator from the resolved-project envelope + wires the host-coupled seams (the host callbacks the render still needs). It returns the Generator (the build order is the caller's responsibility — plugin-build computes it from the reply, plugin-deploy-pod computes the overlay candies from the live plans).

func (*Generator) BuildStageContext added in v0.2026192.1709

func (g *Generator) BuildStageContext(layer CandyModel, builderName string, builderDef *buildkit.BuilderDef, img *buildkit.ResolvedBox, builderRef string) *spec.BuildStageContext

BuildStageContext builds the render context for a candy's builder stage: stage names, copy source, user, cache mounts, detected manifest + lockfile-aware install command, manylinux fix, build script, and config-detected packages. Relocated from charly (P8); byte-identical.

func (*Generator) BuilderRefForFormat added in v0.2026192.1709

func (g *Generator) BuilderRefForFormat(boxName, format string) string

BuilderRefForFormat returns the full tag of the builder image for a format, or "" when there is no distinct builder. Relocated from charly (P8).

func (*Generator) CandyCopySource added in v0.2026192.1709

func (g *Generator) CandyCopySource(candyRef string) string

CandyCopySource returns the build-context path a candy's files are COPYed from: the staged _candy dir for a remote candy, the default candy/<name>/ for a local candy at the default location, else the relative SourceDir. Relocated (P8).

func (*Generator) CandyNeedsBuilder added in v0.2026192.1709

func (g *Generator) CandyNeedsBuilder(img *buildkit.ResolvedBox, layer CandyModel, builderDef *buildkit.BuilderDef) bool

CandyNeedsBuilder returns true if a candy triggers a builder: a DetectFiles match always triggers; a DetectConfig match triggers only when the image's build formats include that format (a config-only detection is distro-coupled — the IR compiler only emits install steps for formats in img.BuildFormats). Relocated from charly (P8); byte-identical (buildFormatsInclude inlined as slices.Contains).

func (*Generator) CollectBuilderRuntimeEnv added in v0.2026192.1709

func (g *Generator) CollectBuilderRuntimeEnv(candyOrder []string, img *buildkit.ResolvedBox) []*kit.EnvConfig

CollectBuilderRuntimeEnv gathers the runtime env + PATH contributions declared by each builder a candy in the image triggers (via CandyNeedsBuilder). Relocated (P8).

func (*Generator) EmitBaseBootstrap added in v0.2026192.1709

func (g *Generator) EmitBaseBootstrap(b *strings.Builder, boxName string, img *buildkit.ResolvedBox)

EmitBaseBootstrap emits the base-stage bootstrap: a builder-rootfs ADD for a `from: builder:` base, then either the full WriteBootstrap (external base) or a USER root reset (internal base). Relocated from charly (P8); byte-identical.

func (*Generator) EmitBuilderArtifacts added in v0.2026194.1727

func (g *Generator) EmitBuilderArtifacts(b *strings.Builder, img *buildkit.ResolvedBox, candyOrder []string)

EmitBuilderArtifacts copies builder artifacts/binaries into the main image. For an EXTERNALIZED detection builder (pixi/npm/aur) the COPY lines come from the plugin's cached OpResolve reply (detectionBuilderReplies, populated by EmitBuilderStages) — the plugin owns the copy_artifact/copy_binary shape now (C10). For a custom (non-externalized) builder they come from the embedded builder: vocabulary copy_artifacts/copy_binary. Both preserve the former structure: the `# Copy <builder> artifacts` header once per builder, then each candy's artifacts, and the builder BINARY copied ONCE per builder (deduped across candies).

func (*Generator) EmitBuilderStages added in v0.2026194.1727

func (g *Generator) EmitBuilderStages(b *strings.Builder, boxName string, img *buildkit.ResolvedBox, candyOrder []string) error

EmitBuilderStages emits per-candy multi-stage build stages. Each builder declares detect_files and/or detect_config (the embedded builder: vocabulary, host-side DETECTION); for each matching candy an EXTERNALIZED detection builder (pixi/npm/aur) renders its stage via its plugin's OpResolve leg (ResolveDetectionBuilderStage → kit.BuilderResolve, C10 — no longer an in-core stage template); a non-externalized builder emits no build-time multi-stage (a custom builder must be an external_builder plugin). The externalized builder plugins are connected on-demand (EnsureBuildersConnected). The reply is cached per (candy, builder) for EmitBuilderArtifacts.

func (*Generator) EmitExternalBuilderArtifacts added in v0.2026194.1727

func (g *Generator) EmitExternalBuilderArtifacts(b *strings.Builder, candyOrder []string)

EmitExternalBuilderArtifacts writes the post-main-FROM COPY --from directives for each candy whose external builder resolved in EmitExternalBuilderStages (read from the per-image cache — never re-Invoked). The artifacts pull the built files out of the plugin's multi-stage build into the final image.

func (*Generator) EmitExternalBuilderStages added in v0.2026194.1727

func (g *Generator) EmitExternalBuilderStages(b *strings.Builder, img *buildkit.ResolvedBox, candyOrder []string) error

EmitExternalBuilderStages emits the pre-main-FROM multi-stage block for every candy that selects an `external_builder:` — the build-time BUILDER leg, the multi-stage counterpart of a `run:` step's `plugin:` verb. For each such candy it resolves the builder word through the ResolveExternalBuilderStage seam (registry ResolveBuilder + *grpcProvider assertion + OpResolve Invoke, charly-side, all error strings byte-preserved) and writes the returned BuilderResolveReply's Stage verbatim (egress-validated with the rest of the Containerfile). The reply is CACHED on the Generator so EmitExternalBuilderArtifacts can splice the matching COPY --from directives post-main-FROM without re-Invoking. The cache is RESET here so it scopes to ONE image.

func (*Generator) EmitExtractStages added in v0.2026192.1709

func (g *Generator) EmitExtractStages(b *strings.Builder, candyOrder []string)

EmitExtractStages emits a `FROM <source> AS <candy>-extract-<i>` stage per candy extract directive (the source images files are extracted from). (P8).

func (*Generator) EmitExtractedFiles added in v0.2026192.1709

func (g *Generator) EmitExtractedFiles(b *strings.Builder, img *buildkit.ResolvedBox, candyOrder []string)

EmitExtractedFiles emits the COPY --from=<extract-stage> directives that pull extracted files into the image (chowned to the image user). (P8).

func (*Generator) EmitInitAssembly added in v0.2026192.1709

func (g *Generator) EmitInitAssembly(b *strings.Builder, img *buildkit.ResolvedBox, candyOrder []string, activeInits map[string]*spec.ResolvedInit, initHasFragments map[string]bool) error

EmitInitAssembly emits the assembly_template, system_enable_template, and post_assembly_template RUN steps for each active init system. Relocated from charly (P8); byte-identical. initHasFragments gates the assembly step (a fragment-less init contributed no scratch stage to bind-mount from).

func (*Generator) EmitInitFragmentStages added in v0.2026192.1709

func (g *Generator) EmitInitFragmentStages(b *strings.Builder, boxName string, img *buildkit.ResolvedBox, candyOrder []string, activeInits map[string]*spec.ResolvedInit) (map[string]bool, error)

EmitInitFragmentStages emits the per-init scratch stages that COPY service fragments, relay configs, and detected service files, and returns the per-init map of whether any fragment content was contributed (consumed by EmitInitAssembly). Relocated from charly (P8); byte-identical.

func (*Generator) EmitPreMainCandyStages added in v0.2026194.1727

func (g *Generator) EmitPreMainCandyStages(b *strings.Builder, boxName string, img *buildkit.ResolvedBox, candyOrder []string) error

EmitPreMainCandyStages emits the pre-main-FROM per-candy stages: the FROM-scratch COPY stages, the detection-builder multi-stage build stages (fully config-driven from the embedded builder: vocabulary), the EXTERNAL builder stages (a candy selecting an `external_builder:` gets its provider's OpResolve stage spliced pre-main-FROM; the artifacts COPY follows post-main-FROM via EmitExternalBuilderArtifacts), and the extraction stages.

func (*Generator) EmitScratchStages added in v0.2026192.1709

func (g *Generator) EmitScratchStages(b *strings.Builder, candyOrder []string)

EmitScratchStages emits a `FROM scratch AS <candy>` + `COPY <src>/ /` per candy, staging each candy's files in its own scratch layer. Relocated (P8).

func (*Generator) EmitTasks added in v0.2026192.1709

func (g *Generator) EmitTasks(b *strings.Builder, layer CandyModel, img *buildkit.ResolvedBox, ops []vmshared.Op, buildDir, contextRelPrefix string) (string, error)

emitTasks renders a candy's tasks to b in strict order, with adjacent-coalescing for mkdir/link/setcap batches, parent-dir auto-insertion for copy/write, USER switches on user change, and implicit build-task auto-append. Returns the final USER (so writeCandySteps knows whether to reset the candy boundary). Relocated from charly core (P8); byte-identical. The `plugin` verb case is the ONLY host seam — dispatched through g.EmitPluginOp (the Provider registry stays core).

func (*Generator) EmitTraefikRouteStage added in v0.2026192.1709

func (g *Generator) EmitTraefikRouteStage(b *strings.Builder, boxName string, img *buildkit.ResolvedBox, candyOrder []string) (hasRoutes, hasTraefik bool, err error)

EmitTraefikRouteStage emits the traefik-routes scratch stage when the image has route candies AND traefik, generating the routes file. Returns (hasRoutes, hasTraefik). Relocated from charly (P8); byte-identical.

func (*Generator) Generate added in v0.2026195.1912

func (g *Generator) Generate(order []string) error

Generate renders a Containerfile for each box in order, writing them to g.BuildDir/<name>/Containerfile and caching the content in g.Containerfiles. The caller (charly's hostBuildBuildResolve generate-prep, or plugin-build's runBoxGenerate/runBoxBuild) provides the order; the build-PREP (cleanStaleBuildDirs, MkdirAll, writeContextIgnore, createRemoteCandyCopies, ResolveBoxOrder, filterBox, resolveUserContext) stays HOST — Generate does ONLY the per-box render.

func (*Generator) GenerateInitFragments added in v0.2026192.1709

func (g *Generator) GenerateInitFragments(boxName, initName string, def *spec.ResolvedInit, candyOrder []string) error

GenerateInitFragments writes init system config fragments to .build/<image>/<fragmentDir>/. Schema-driven: iterates each candy's service: list and renders every entry that binds to this init via per-entry routing (use_packaged → systemd; custom exec → any init with a service_template). Relocated from charly (P8); byte-identical. The service render itself crosses to candy/plugin-init via the RenderService seam.

func (*Generator) GenerateTraefikRoutes added in v0.2026192.1709

func (g *Generator) GenerateTraefikRoutes(boxName string, candyOrder []string, _ *buildkit.ResolvedBox) error

GenerateTraefikRoutes writes the image's traefik dynamic-config (routers + services) to .build/<box>/traefik-routes.yml, egress-gated before write. Relocated from charly (P8); byte-identical.

func (*Generator) GlobalOrderForBox added in v0.2026192.1709

func (g *Generator) GlobalOrderForBox(imageCandies []string, parentCandies map[string]bool) ([]string, error)

GlobalOrderForBox returns the candy order for an image by filtering the global order to only the image's needed candies (expanded + transitively resolved). Shared candies keep the same cross-image order, maximizing cache reuse. Any needed candy missing from the global order is appended in resolution order (a safety net that shouldn't trigger). Relocated from charly core (P8); byte-identical.

func (*Generator) RenderPrepAll added in v0.2026209.2002

func (g *Generator) RenderPrepAll() error

RenderPrepAll runs RenderPrepBox for every box in the generator's box set. Called by the host build-prep seam before projecting the envelope.

func (*Generator) RenderPrepBox added in v0.2026209.2002

func (g *Generator) RenderPrepBox(boxName string) error

RenderPrepBox fills the build-render caches on g.Boxes[boxName] by reading the live graph. Returns the first error (caps missing etc.).

func (*Generator) ResolveBaseImage added in v0.2026192.1709

func (g *Generator) ResolveBaseImage(img *buildkit.ResolvedBox) string

ResolveBaseImage returns the base image ref for img: the external OCI ref when external, else the parent image's full CalVer tag. Relocated from charly (P8).

func (*Generator) WriteBootstrap added in v0.2026192.1709

func (g *Generator) WriteBootstrap(b *strings.Builder, img *buildkit.ResolvedBox)

WriteBootstrap emits the base-image bootstrap block: cache mounts, dnf tuning, bootstrap package install, distro workarounds, the go-task install, user/group creation (or adoption), and WORKDIR. Relocated from charly (P8); byte-identical.

func (*Generator) WriteCandyEnv added in v0.2026192.1709

func (g *Generator) WriteCandyEnv(b *strings.Builder, candyOrder []string, img *buildkit.ResolvedBox)

WriteCandyEnv emits the merged candy env + builder runtime env as sorted ENV directives (plus a PATH-append). Relocated from charly (P8); byte-identical (sortStrings → slices.Sort is the same lexicographic order).

func (*Generator) WriteCandySteps added in v0.2026192.1709

func (g *Generator) WriteCandySteps(b *strings.Builder, candyName string, img *buildkit.ResolvedBox, skipRootReset bool) (bool, error)

WriteCandySteps writes the RUN steps for a single candy (the per-candy render call-graph: ENV/packages/localpkg/tasks/builders/shell/user-reset sharing the asUser state). Relocated from charly (P8); byte-identical. skipRootReset prevents emitting USER root after user-mode steps (the last candy when no post-candy root steps follow); returns true if the candy ended in user mode.

func (*Generator) WriteDataStaging added in v0.2026192.1709

func (g *Generator) WriteDataStaging(b *strings.Builder, candyOrder []string, img *buildkit.ResolvedBox)

WriteDataStaging emits COPY --from=<candy-scratch-stage> directives that stage a candy's data/ tree under /data/<volume>/[dest] for deploy-time provisioning into bind-backed volumes. Uses the short stage alias (GetName) — a remote candy's map key is not a valid build-stage ref. Relocated from charly (P8); byte-identical.

func (*Generator) WriteExpose added in v0.2026192.1709

func (g *Generator) WriteExpose(b *strings.Builder, boxName string)

WriteExpose emits EXPOSE directives for an image's aggregated ports (via the CollectBoxPorts seam). Relocated from charly (P8); byte-identical.

func (*Generator) WriteLabels added in v0.2026195.1912

func (g *Generator) WriteLabels(b *strings.Builder, meta *spec.BakedLabelSet, boxName string)

WriteLabels emits the image metadata LABEL block from a fully-baked *spec.BakedLabelSet. It is the format-only mirror of the former charly writeLabels: it reads NO live graph, only the carrier, so it is placement-agnostic (the live render path and the plugin-build envelope path feed it the same struct). LABELs are emitted LAST in the final stage (the caller places the call after the final USER directive) so a test/label edit only reruns the LABEL instructions (metadata-only) instead of invalidating the buildkit cache for every upstream RUN/COPY.

type HooksConfig added in v0.2026190.848

type HooksConfig = spec.HooksConfig

func CollectHooks added in v0.2026209.2002

func CollectHooks(cfg *spec.Config, layers map[string]CandyModel, boxName string) *HooksConfig

CollectHooks collects and concatenates hooks from all candies in a box's candy chain, in candy order. Relocated from charly/hooks.go in the core-min wave-3 build-cluster split. The candy chain resolution (BoxCandyChain) and the concatenation (MergeCandyHooks) are both pure sdk.

func MergeCandyHooks added in v0.2026198.2057

func MergeCandyHooks(candies []CandyModel) *HooksConfig

MergeCandyHooks concatenates PostEnable/PreRemove hook scripts across an ordered candy chain, one section per script kind, newline-joined in candy order. Returns nil when no candy in the chain declares any hook (matches the pre-split charly/hooks.go CollectHooks).

type HostContext added in v0.2026190.848

type HostContext struct {
	// MachineVenue selects compilation mode (P9): false (zero value) → compile for a
	// CONTAINER image build (the pod overlay / OCI target); true → compile for a MACHINE
	// venue with a system init (a target:local / target:vm deploy — services render as
	// systemd units, home is deferred via {{.Home}}). detectHostContext sets it true for a
	// host deploy; the OCI/pod-overlay compile passes the zero value. It replaced the former
	// string Target ("host"/"vm"/"oci"), whose "vm" arm was dead (a vm deploy compiles with
	// the host detectHostContext) — the machine-vs-container distinction IS the trait.
	MachineVenue bool

	// Distro is the resolved host distro tag, e.g. "fedora:43". Used to
	// pick the right format section when compiling for a host target
	// whose distro differs from the image's primary distro.
	Distro string

	// GlibcVersion is the host's glibc major.minor as reported by
	// `ldd --version`. Used by the host target's preflight check against
	// the selected builder image. Optional; empty means skip the check.
	GlibcVersion string

	// BuilderImage overrides the default builder-image selection for
	// VenueContainerBuilder steps. Populated from --builder-image. ""
	// means "use the embedded build vocabulary's default".
	BuilderImage string

	// BuilderContext carries the host-side build PRE-PASS result: each externalized
	// detection-builder's per-candy stage context + teardown ops, keyed by
	// BuilderCtxKey(candy, builder). Populated by preresolveBuilderContexts BEFORE
	// this pure compile (the deploy command path); read by collectBuilderContext +
	// compileBuilderSteps so the compiler NEVER dials a builder plugin (purity). Nil
	// when no pre-pass ran (a direct BuildDeployPlan caller / test) or no externalized
	// builder is triggered → the affected builder gets base-only context, no teardown.
	BuilderContext map[string]BuilderPreresolved

	// ActiveInitName/ActiveInit carry the MachineVenue's preresolved active init system —
	// populated ONCE per whole-deploy compile by the deploy-compile seam's by-name,
	// existence-checked lookup (bundle_compile_seam.go's preresolveActiveInitInto), alongside
	// the BuilderContext pre-pass. compileServiceSteps reads these instead of re-deriving the
	// active init per-candy or guessing via a container-oriented auto-detect heuristic (which
	// cannot disambiguate a machine venue's init from a plain custom-exec service entry — proven
	// live 2026-07-20). Nil/empty for a direct BuildDeployPlan caller / test that compiles
	// outside the seam; compileServiceSteps falls back to its own lazy per-call lookup then.
	ActiveInitName string
	ActiveInit     *spec.ResolvedInit
}

HostContext carries host-side information the compiler needs to decide (a) which format section to pick for the host target, (b) which builder image to run user-scope builders in, (c) whether to gate AUR-specific steps, etc. For the OCI target, the caller passes a zero-value HostContext (the compiler ignores host-only choices when MachineVenue is false).

type HostReverseExec added in v0.2026201.1934

type HostReverseExec struct {
	DryRun          bool
	KeepRepoChanges bool
	KeepServices    bool
	Runner          kit.ReverseRunner
}

HostReverseExec is the ReverseExecutor adapter combining a teardown's gate flags with a per-call DryRun + ReverseRunner. Used by the host-teardown path (externalDeployTarget.Del for the local/external substrate).

func (*HostReverseExec) ReverseDryRun added in v0.2026201.1934

func (e *HostReverseExec) ReverseDryRun() bool

func (*HostReverseExec) ReverseKeepRepoChanges added in v0.2026201.1934

func (e *HostReverseExec) ReverseKeepRepoChanges() bool

func (*HostReverseExec) ReverseKeepServices added in v0.2026201.1934

func (e *HostReverseExec) ReverseKeepServices() bool

func (*HostReverseExec) ReverseRunner added in v0.2026201.1934

func (e *HostReverseExec) ReverseRunner() kit.ReverseRunner

type InlineBuilderParams added in v0.2026195.1912

type InlineBuilderParams struct {
	Dir         string                  `json:"dir"`
	BoxName     string                  `json:"box_name"`
	CandyName   string                  `json:"candy_name"`
	BuilderName string                  `json:"builder_name"`
	BDef        *spec.Builder           `json:"b_def"`
	Ctx         *spec.BuildStageContext `json:"ctx"`
}

InlineBuilderParams carries the inputs to core resolveInlineBuilderSeam. BDef is spec.Builder (BuilderDef = spec.Builder); Ctx is spec.BuildStageContext — both CUE-sourced, json-tagged.

type InlineBuilderResult added in v0.2026195.1912

type InlineBuilderResult struct {
	Fragment string `json:"fragment"`
}

InlineBuilderResult carries the inline fragment.

type InstallOptsConfig added in v0.2026190.848

type InstallOptsConfig = vmshared.InstallOptsConfig

type InstallPlan added in v0.2026190.848

type InstallPlan = spec.InstallPlan

func BuildDeployPlan added in v0.2026190.848

func BuildDeployPlan(ctx context.Context, ex *sdk.Executor, layer CandyModel, img *ResolvedBox, hostCtx HostContext) (*InstallPlan, error)

BuildDeployPlan compiles one Candy into an InstallPlan.

For whole-image deploys, the caller iterates the ordered candy list (from ResolveCandyOrder) and builds one plan per candy, then merges them. Keeping one plan per candy makes refcounting trivial in the ledger: each candy's teardown is independent.

func MergePlan added in v0.2026190.848

func MergePlan(plans []*InstallPlan, image string, addCandies []string) *InstallPlan

MergePlan combines a list of per-candy plans into one whole-image plan. Used by deploy targets that want to see all steps in one sequence (for sudo batching, overall dry-run display, etc.) while preserving per-candy provenance for the ledger.

type InstallStep added in v0.2026190.848

type InstallStep = spec.InstallStep

func CompileApkStep added in v0.2026190.848

func CompileApkStep(layer CandyModel) InstallStep

CompileApkStep turns a candy's `apk:` package list into a single ApkInstallStep, or nil if the candy declares no apk packages. The `apk` format is target-agnostic at compile time — the step carries the specs and each DeployTarget decides whether to execute (android) or skip (everything else), exactly as the IR intends for venue-specific work.

func CompileLocalPkgStep added in v0.2026190.848

func CompileLocalPkgStep(layer CandyModel, img *ResolvedBox, _ HostContext) InstallStep

CompileLocalPkgStep turns a candy's `localpkg:` field into a single LocalPkgInstallStep, or nil if the candy declares none. Like CompileApkStep it is target-agnostic at compile time — the step carries the author's PKGBUILD hint plus the candy dir AND the deploy project dir (os.Getwd, the same handle compileServiceSteps reads) as the two anchors the emit-time walk-up search uses to locate the PKGBUILD. Each DeployTarget decides whether to build+install (localpkg-capable host/guest), skip (image build, non-pac targets, android, k8s).

The localpkg mechanism is fully config-driven: the format's `local_pkg:` block (resolved here via DistroDef.LocalPkgFormat, the SAME DistroDef the system-package steps read) supplies the build/install templates, package glob, foreign-deps query, probe, and dependency-builder name. BuilderImage is resolved for that block's DepBuilder the same way builder steps resolve theirs (resolveBuilderImage) so the executor can build the package's dependency closure through the EXISTING builder before installing. When the distro declares no localpkg-capable format, LocalPkg is nil and the executor skips; when no dep builder resolves, BuilderImage is "" and the dep-build is skipped with a clear log (the candy's own curl/COPY fallback still covers non-localpkg targets).

func CompileOpSteps added in v0.2026190.848

func CompileOpSteps(ctx context.Context, ex *sdk.Executor, layer CandyModel, img *ResolvedBox) ([]InstallStep, error)

CompileOpSteps turns the candy's install timeline into InstallSteps. The timeline is the candy's `plan:` `run:` steps (StepKind()==KwRun) whose context includes build/deploy, in declared (cache-stable) order — the fold that retired the separate `task:` list. Each run op either LOWERS into an existing typed step (the `plugin: package` verb → SystemPackagesStep and the `plugin: service` verb → ServicePackagedStep, each via its TypedStepProvider) so emit + reversal are REUSED, or stays a generic OpStep (install verbs + command + the RenderProvisionScript plugin verbs). A run: step scoped runtime-only is plan-runtime provisioning the check Runner executes — NOT the install timeline — and is skipped here (avoids double-execution); check:/agent-*/ include: steps are never lowered.

func CompileServiceSteps added in v0.2026190.848

func CompileServiceSteps(ctx context.Context, ex *sdk.Executor, layer CandyModel, img *ResolvedBox, hostCtx HostContext) ([]InstallStep, error)

CompileServiceSteps lowers a candy's service: list into install steps.

func CompileShellSnippetSteps added in v0.2026190.848

func CompileShellSnippetSteps(layer CandyModel, img *ResolvedBox, hostCtx HostContext) []InstallStep

CompileShellSnippetSteps returns one ShellSnippetStep per (candy, shell) pair the candy contributes. Selection rule (mirrors TagPkgConfig):

  1. If layer.Shell.ByShell[shell] exists, render it.
  2. Else if layer.Shell.Init is non-empty, render the generic body with ${SHELL_NAME} substituted to the active shell name.
  3. Else this shell gets nothing from this candy.

path_append entries are rendered into the snippet body using shell-appropriate syntax (PATH=... for bash/zsh/sh, fish_add_path for fish) before the step is emitted, so each DeployTarget emitter can write the snippet bytes verbatim.

Destination resolution is target-aware:

  • Container build (hostCtx.MachineVenue == false): system-wide drop-in (/etc/profile.d/charly-<candy>-<shell>.sh, /etc/fish/conf.d/ charly-<candy>.fish). UseDropin=true.
  • target:local, target:vm (hostCtx.MachineVenue == true): bash/zsh/sh → managed-block append in the user's rc file (UseDropin =false); fish → per-candy drop-in in ~/.config/fish/conf.d/ (UseDropin=true).

Returns nil when the candy has no shell: block.

func CompileSystemPackageSteps added in v0.2026190.848

func CompileSystemPackageSteps(layer CandyModel, img *ResolvedBox, _ HostContext) []InstallStep

CompileSystemPackageSteps emits SystemPackagesStep(s) for the candy's package sections, honoring the distro-tag-wins-over-build-format rule from today's writeCandySteps.

Phase 1: walk img.Distro tags in order; first match wins. If a tag section is found, emit ONE install step using that section and stop.

Phase 2: if no tag matched, walk img.BuildFormats in order; emit one install step per format section that has packages. (Today's writeCandySteps does this to support images that install both rpm and aur packages, for example.)

For the IR we additionally break each install into the three-phase structure (prepare/install/cleanup) so the host target can gate PhasePrepare on --allow-repo-changes. The embedded vocabulary (charly/charly.yml) currently has only one phase per format (the monolithic install_template); Task 4 will split templates into phases. Until then, we emit everything as PhaseInstall. CompileSystemPackageSteps resolves a candy's package surface for an image via the distro-specificity CASCADE and emits ONE SystemPackagesStep for the image's primary format. img.Distro is the most-specific-first tag chain (e.g. [debian:13, debian] or [ubuntu:24.04, ubuntu]); the candy's top-level package: list (layer.TopPackages) is the always-included BASE.

  • packages = UNION of the base + every matching tag section's packages (dedup), so a shared package at one level plus extras at another accumulate.
  • repo/copr/options/exclude/module = the MOST-SPECIFIC matching level that declares the field wins (a version's repo overrides the bare distro's).

img.Distro is most-specific-first, so iterating it in REVERSE (least → most specific) and letting later writes win yields most-specific-wins for the Raw extras while packages union regardless of order. fedora/arch reach their packages via the bare-distro tag (img.Distro = [fedora] / [arch]); there is no format-section fallback — the deb collapse that caused non-deterministic repo selection is gone (per-distro tag sections never share a mutable section).

type IntermediateDefaults added in v0.2026198.914

type IntermediateDefaults struct {
	Builder   buildkit.BuilderMap
	UID       *int
	User      string
	GID       *int
	Merge     *buildkit.MergeConfig
	Registry  string
	Platforms []string
	Distro    []string
	Build     []string
}

IntermediateDefaults carries the scalar `defaults:` fields ComputeIntermediates needs from the project config — exactly the fields the former charly/intermediates.go read off *Config (cfg.Defaults.*). A plain data carrier (no loader/registry access), so ComputeIntermediates stays a pure function of its inputs.

type JumpKind added in v0.2026190.848

type JumpKind = kit.JumpKind

type LocalPkgDef

type LocalPkgDef = spec.LocalPkg

type LocalPkgInstallStep

type LocalPkgInstallStep = spec.LocalPkgInstallStep

type MCPProvideEntry added in v0.2026190.848

type MCPProvideEntry = spec.MCPProvideEntry

type MCPServerYAML added in v0.2026190.848

type MCPServerYAML = spec.MCPServerYAML

type Named added in v0.2026196.928

type Named interface {
	GetName() string
	GetSource() string
}

Named is the interface for provides entries (shared pipeline logic). spec.EnvProvideEntry and MCPProvideEntry both satisfy it via their GetName/GetSource methods. Relocated from charly/provides.go so RemoveBySource/RemoveByExactSource (used by CleanDeployEntry) stay in the same package as their caller.

type NestedExecutor added in v0.2026190.848

type NestedExecutor = kit.NestedExecutor

type NestedJump added in v0.2026190.848

type NestedJump = kit.NestedJump

type OCIEmitStepParams added in v0.2026196.1550

type OCIEmitStepParams = spec.OCIEmitStepParams

OCIEmitStepParams is the per-step pod-overlay "oci-emit-step" HostBuild payload.

type OCITarget added in v0.2026196.1550

type OCITarget struct {
	// Dir is the project dir — the key the EmitStepOp seam (HostBuild("render-seam",
	// "oci-emit-step")) uses to look up the host-side cached overlay buildEngineContext (the
	// host overlay-prep constructs + caches it per dir, mirroring renderGenCache for the box
	// build). The live *Generator / DistroDef / BuilderConfig / Box / ImageBuildDir /
	// ContextRelPrefix the host step-emitters need never cross the wire — only this dir key does.
	Dir string

	// Home is the overlay base image's runtime home — read by the walker for ResolveHome (the
	// `{{.Home}}` token in home-bearing step fields resolves to this, the home the baked paths
	// run under). The host overlay-prep returns it to the candy (via a render-seam method); the
	// walker never holds the full ResolvedBox.
	Home string

	// Distros is the overlay base image's distro tag set (Box.Tags) — threaded onto each step's
	// EmitStepOp seam call so the host class:step OpEmit can gate a plugin's build-emit on the
	// image's distro set (the SAME datum the former in-core OCITarget passed to spliceClassStepEmit).
	Distros []string

	// EmitStepOp is the host-coupled step-render seam: it renders ONE InstallStep's
	// Containerfile fragment via the CORE provider-registry dispatch (spliceClassStepEmit for
	// the 12 compiler-emitted plugin-served kinds + the authored external step; stepProviderFor
	// .EmitOCI for ExternalPlugin), using the host-side overlay Generator + buildEngineContext
	// cached by overlay-prep. The candy wires this via HostBuild("render-seam","oci-emit-step")
	// (out-of-process) or an in-proc closure (compiled-in / tests). A step that emits nothing
	// (a deploy-only step, or VenueSkip) returns "". Nil EmitStepOp => a no-op render (the walker
	// still writes the `# Layer:` header + the home resolution; tests that exercise the dispatch
	// wire a real seam or stay in core).
	EmitStepOp func(step spec.InstallStep, plan *spec.InstallPlan, distros []string) (string, error)
	// contains filtered or unexported fields
}

OCITarget emits Containerfile directives for an InstallPlan. One instance handles one overlay build; the caller (candy/plugin-deploy-pod's podPrepareVenue) constructs a target, calls Emit with the add_candy plans, and reads the rendered fragment via String.

func (*OCITarget) Emit added in v0.2026196.1550

func (t *OCITarget) Emit(plans []*spec.InstallPlan, _ spec.EmitOpts) error

Emit walks each plan's steps and appends Containerfile directives to the internal buffer. Multiple plans emit sequentially (per-candy), mirroring the pre-move in-core OCITarget.Emit.

func (*OCITarget) Name added in v0.2026196.1550

func (t *OCITarget) Name() string

Name identifies this target.

func (*OCITarget) String added in v0.2026196.1550

func (t *OCITarget) String() string

String returns the accumulated Containerfile fragment.

type Op

type Op = spec.Op

type OpStep

type OpStep = spec.OpStep

type PackageItem added in v0.2026190.848

type PackageItem = vmshared.PackageItem

type PackageSection added in v0.2026190.848

type PackageSection = spec.PackageSection

PackageSection — a generic format-specific package section (rpm/deb/pac/aur). Raw carries the full YAML map for template rendering.

type Phase

type Phase = spec.Phase

type PortSpec added in v0.2026190.848

type PortSpec = spec.PortSpec

type PreemptibleConfig added in v0.2026190.848

type PreemptibleConfig = spec.PreemptibleConfig

type QuadletConfig added in v0.2026194.615

type QuadletConfig struct {
	BoxName         string
	ImageRef        string
	Home            string
	Ports           []string
	Volumes         []VolumeMount
	BindMounts      []ResolvedBindMount
	GPU             bool
	BindAddress     string
	Tunnel          *spec.TunnelConfig
	UID             int
	GID             int
	Env             []string
	EnvFile         string
	Instance        string
	Security        vmshared.SecurityConfig
	Network         string
	Version         string
	Status          string
	Info            string
	Secrets         []CollectedSecret
	Entrypoint      []string
	CharlyBin       string
	EncryptedMounts bool
	KeyringBackend  bool
	PodName         string
	Sidecar         []ResolvedSidecar
}

QuadletConfig holds the parameters for generating a quadlet .container file. An INTERNAL library type (ruling C): the plugin constructs + consumes it in one process (built from config-resolve data), so it never crosses the host↔plugin boundary and needs no CUE-sourcing.

type RebootStep

type RebootStep = spec.RebootStep

type RelayContext added in v0.2026192.1709

type RelayContext struct {
	Port      int
	CandyName string
	Index     int
}

RelayContext is the template context for relay_template rendering.

type RepoChangeStep

type RepoChangeStep = spec.RepoChangeStep

type RepoSpec

type RepoSpec = spec.RepoSpec

The step VOCABULARY (relocated to spec, #55 step-4 — in-proc IR).

type ResolvedBindMount added in v0.2026194.615

type ResolvedBindMount struct {
	Name      string // e.g. "secrets"
	HostPath  string // effective host path (plain: expanded host, encrypted: plain dir)
	ContPath  string // container path (expanded)
	Encrypted bool   // for status/mount checks
}

ResolvedBindMount is a deploy-time resolved bind mount (host-path state). Runtime-only (never baked into an OCI label — LabelVolumeEntry is the label type), so it lives here as a plain deploykit struct (no CUE). Relocated from charly/enc.go with the P11 enc-model move.

type ResolvedBox added in v0.2026190.848

type ResolvedBox = buildkit.ResolvedBox

type ResolvedSidecar added in v0.2026194.615

type ResolvedSidecar struct {
	Name     string                  // sidecar key (e.g., "tailscale")
	Image    string                  // resolved OCI image ref
	Env      map[string]string       // merged env vars
	Secret   []CollectedSecret       // provisioned podman secrets
	Volume   []VolumeMount           // resolved named volumes
	Security vmshared.SecurityConfig // merged security config
}

ResolvedSidecar is a fully-resolved sidecar ready for quadlet generation — the host-adapted form (the sidecar plugin's spec.ResolvedSidecar wire type is adapted on the host into these deploykit/vmshared runtime types). Resolved runtime state (ruling C), not a wire type, so it lives here as a plain struct beside the other pod config-write inputs. Consumed by the pod quadlet generators (generatePodQuadlet / generateSidecarQuadlet) that assemble the .pod + sidecar .container files.

func ResolvedSidecarFromSpec added in v0.2026198.358

func ResolvedSidecarFromSpec(s spec.ResolvedSidecar) ResolvedSidecar

ResolvedSidecarFromSpec adapts one plugin-resolved spec.ResolvedSidecar into the host's ResolvedSidecar (the quadlet-gen shape).

type ReverseOp

type ReverseOp = spec.ReverseOp

type ReverseOpKind

type ReverseOpKind = spec.ReverseOpKind

type RouteConfig added in v0.2026190.848

type RouteConfig = spec.RouteConfig

RouteConfig — a resolved route file declaration (host + port-as-string).

type SSHExecutor added in v0.2026190.848

type SSHExecutor = kit.SSHExecutor

func SSHParamsForVm added in v0.2026190.848

func SSHParamsForVm(domainID string) *SSHExecutor

SSHParamsForVm returns an SSHExecutor pointing at the VM's managed ssh-config alias (charly-<domainID>) — the caller passes the per-deploy DOMAIN IDENTITY (VmDomainIdentity of the deploy), NOT the shared kind:vm entity (P33). All connection details — User, Port, IdentityFile, host-key checking — live in the Host stanza that `charly vm create` / `charly bundle add` published into ~/.config/charly/ssh_config; ssh(1) reads them from there. Our SSHExecutor needs only the alias as Host.

type SaveDeployStateInput added in v0.2026196.928

type SaveDeployStateInput = spec.SaveDeployStateInput

SaveDeployStateInput holds the parameters SaveDeployState (in this package) persists.

type Scope

type Scope = spec.Scope

type SecretResolution added in v0.2026202.1112

type SecretResolution struct {
	Name     string // env var name (e.g., "OPENROUTER_API_KEY")
	Source   string // resolver source classification (env/keyring/config/locked/unavailable/default)
	Resolved bool   // true iff a non-empty value was obtained
	Required bool   // true iff the entry came from secret_requires (not secret_accepts)
}

SecretResolution records the result of resolving a single secret_accepts or secret_requires entry against the credential store. Returned alongside the []CollectedSecret list from CollectCandySecretAccepts so downstream callers (charly's checkMissingSecretRequires) can distinguish "required but missing" from "optional and absent" with actionable remediation.

type SecretYAML added in v0.2026190.848

type SecretYAML = spec.SecretYAML

type SecurityConfig added in v0.2026190.848

type SecurityConfig = spec.SecurityConfig

func CollectSecurity added in v0.2026209.2002

func CollectSecurity(cfg *spec.Config, layers map[string]CandyModel, boxName string) SecurityConfig

CollectSecurity resolves a box's own candy tree (leaf-specific — security does NOT inherit from a base box; the shared BoxDirectCandies walk), then folds every candy's security config via MergeCandySecurity and applies the box-level override. Relocated from charly/security.go in the core-min wave-3 build-cluster split (Config = spec.Config, BoxDirectCandies is an sdk mechanism, so the whole aggregator is pure).

func MergeCandySecurity added in v0.2026198.2057

func MergeCandySecurity(candies []CandyModel, override *SecurityConfig) SecurityConfig

MergeCandySecurity folds a box's ordered candy security configs into one SecurityConfig, then applies the box-level override — the pure half of charly's CollectSecurity (candy_chain resolution + the *Config/BoxConfig lookup stay host-side; this loop is the R-item every OCI-label-collector build-render consumer — host TODAY, an out-of-process build/deploy plugin tomorrow — can share). Merge rules (unchanged from the pre-split charly/security.go): if any candy sets privileged: true, the result is privileged; cap_add/devices/security_opt/group_add/mounts are unioned; shm_size takes the largest value (biggest-wins — more shared memory is safer); memory_max/memory_high/ memory_swap_max/cpus take the smallest (smallest-wins — a tighter cap is a smaller blast radius). Image-level override replaces rather than merges (last writer, authored intent wins).

type ServiceCustomStep

type ServiceCustomStep = spec.ServiceCustomStep

type ServiceEntry added in v0.2026190.848

type ServiceEntry = spec.ServiceEntry

type ServicePackagedStep

type ServicePackagedStep = spec.ServicePackagedStep

type ShellConfig added in v0.2026190.848

type ShellConfig = vmshared.ShellConfig

type ShellExecutor added in v0.2026190.848

type ShellExecutor = kit.ShellExecutor

type ShellHookStep

type ShellHookStep = spec.ShellHookStep

func CompileShellHookStep added in v0.2026190.848

func CompileShellHookStep(layer CandyModel, _ *ResolvedBox) *ShellHookStep

CompileShellHookStep returns a ShellHookStep for the candy's env: and path_append: fields, or nil if the candy contributes neither.

Home resolution is DEFERRED: `~`/`$HOME` are rewritten to the literal `{{.Home}}` token rather than expanded against img.Home. Each DeployTarget resolves the token at emit time against the home of the actual deploy destination — img.Home for the OCI/pod-overlay build, the host home for the local deploy target, and the GUEST home (via the SSH executor's ResolveHome) for the vm deploy. Baking img.Home here was wrong for VM deploys: the synthetic plan's Home was the host operator's home, so env.d on the guest pointed at /home/<operator> instead of /home/<guest-user>. See InstallPlan.ResolveHome.

type ShellSnippetStep

type ShellSnippetStep = spec.ShellSnippetStep

type ShellSpec added in v0.2026190.848

type ShellSpec = vmshared.ShellSpec

func ResolveShellSpec added in v0.2026190.848

func ResolveShellSpec(cfg *ShellConfig, shell string) (*ShellSpec, string, []string, bool)

ResolveShellSpec applies the per-shell-wins-over-generic selection rule. Returns the resolved spec, the rendered init body (with ${SHELL_NAME} substituted for the generic case), the path_append slice, and whether the shell has anything to contribute. Generic init wins when no per-shell override exists; per-shell wins otherwise.

type SiblingKey added in v0.2026193.1801

type SiblingKey struct {
	Base string
	UID  int
}

SiblingKey identifies a sibling-grouping equivalence class for intermediate computation. Grouping by (Base, UID) — not just Base — ensures that images with different user contexts (e.g. uid=1000 default vs. uid=0 root) don't share an auto-intermediate. Sharing would bake one group's HOME-relative paths (path_append `~/.foo/bin`, env vars using `~` or `$HOME`) into the intermediate's `ENV PATH` directives, leaving the other group with dead PATH entries that can't be overridden by its own ENV emission.

func SortedSiblingKeys added in v0.2026193.1801

func SortedSiblingKeys(m map[SiblingKey][]string) []SiblingKey

SortedSiblingKeys returns the sibling-group keys in a deterministic order — by base name, then UID. Used to make auto-intermediate creation independent of Go's randomized map iteration (see ComputeIntermediates).

type StageFragmentContext added in v0.2026192.1709

type StageFragmentContext struct {
	BoxName     string
	FragmentDir string
	FileName    string
}

StageFragmentContext is the template context for stage_fragment_copy rendering.

type StateHostMechanisms added in v0.2026196.928

type StateHostMechanisms struct {
	// LoadUnifiedBundleConfig loads a per-host charly.yml configDir through the unified
	// loader and returns its ProjectBundleConfig (or nil, nil when absent / empty).
	LoadUnifiedBundleConfig func(configDir string) (*BundleConfig, error)
}

StateHostMechanisms carries the ONE host-side Mechanism the state-file load/save bodies need but the SDK cannot import: the unified LOADER (LoadUnified → uf.ProjectBundleConfig), reached through LoadUnifiedBundleConfig. charly/ fills it at init (RegisterDeployStateHost); a plugin or SDK consumer that does not register it gets nil-safe no-ops from the write paths (the read paths fall back to the kit-only file path when the seam is absent).

E/M justification (the kernel/plugin boundary law): LoadUnifiedBundleConfig is a kind-blind MECHANISM (M) — it loads ANY per-host charly.yml through the unified loader and returns its ProjectBundleConfig, never branching on a deploy kind. It lives in the seam (not in deploykit directly) ONLY because the LoadUnified orchestration has not yet relocated to sdk/loaderkit (K1, task #31): once K1 lands, plugin-bundle calls loaderkit.LoadUnified directly and this seam dies entirely. The deploy-kind-specific marshal is NOT here — it is the caller's responsibility, supplied as a callback to SaveBundleConfig (a kind-blind file shell). Tracked K1-exit task: this seam field is deleted when K1 lands.

var DeployStateHost *StateHostMechanisms

DeployStateHost is the seam charly fills at init. Nil-safe: the write paths (SaveDeployState / CleanDeployEntry) no-op when it is nil; LoadBundleConfig returns (nil, nil) (absent file) so the read-only validate/inspect paths work without a registration. A plugin/SDK consumer that never writes the per-host ledger leaves it nil.

type Step added in v0.2026190.848

type Step = vmshared.Step

type StepBatch added in v0.2026190.848

type StepBatch = spec.StepBatch

type StepKind

type StepKind = spec.StepKind

type SystemEnableContext added in v0.2026192.1709

type SystemEnableContext struct {
	Units []string
}

SystemEnableContext is the render context for an init system's system_enable_template (the distro-shipped units to enable). Relocated (P8).

type SystemPackagesStep

type SystemPackagesStep = spec.SystemPackagesStep

func BuildSystemPackagesStep added in v0.2026190.848

func BuildSystemPackagesStep(format string, phase Phase, packages []string, raw map[string]any, cacheMounts []CacheMountDef) *SystemPackagesStep

BuildSystemPackagesStep constructs a SystemPackagesStep from a PackageSection's Raw map, extracting the well-known structured fields (repos, options, copr, modules, exclude, keys) for gate checkuation while also preserving the full Raw map for template rendering later.

type TagPkgConfig added in v0.2026190.848

type TagPkgConfig = spec.TagPkgConfig

TagPkgConfig — a distro/version-specific package section (debian:13, ubuntu:24.04, fedora:43…). Raw captures the full YAML so tag sections carry repos:/options:/keys:.

type TrieNode added in v0.2026193.1801

type TrieNode struct {
	Layer    string               // layer at this position ("" for root)
	Children map[string]*TrieNode // layer name → child node
	Boxes    []string             // user-defined images terminating here
}

TrieNode represents a node in the candy prefix trie.

func NewTrieNode added in v0.2026193.1801

func NewTrieNode(layer string) *TrieNode

NewTrieNode returns an empty trie node labeled with layer.

type Venue

type Venue = spec.Venue

type VolumeMount added in v0.2026193.1222

type VolumeMount = spec.VolumeMount

VolumeMount is a resolved named-volume mount (charly-<deploy>-<name> → container path). CUE-sourced in spec (boxmetadata.cue, P2B) + aliased here; consumers keep using deploykit.VolumeMount unchanged. DISTINCT from spec.ResolvedVolumeMount (the box-aggregate `charly box inspect --format volumes` entry): same two fields, different concept + call sites — do NOT conflate them.

func CollectBoxVolume added in v0.2026209.2002

func CollectBoxVolume(cfg *spec.Config, layers map[string]CandyModel, boxName string, home string, excludeNames map[string]bool) ([]VolumeMount, error)

CollectBoxVolume resolves all volumes for a box by traversing the full box chain (box -> base -> base's base) and collecting volume declarations from all candies. Volumes are deduplicated by name (first declaration wins — outermost box takes priority).

type VolumeYAML added in v0.2026190.848

type VolumeYAML = spec.VolumeYAML

Source Files

Jump to

Keyboard shortcuts

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