deploykit

package
v0.2026218.338 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 40 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.

Index

Constants

View Source
const (
	VenueLocal = kit.VenueLocal

	JumpPodmanExec   = kit.JumpPodmanExec
	JumpDockerExec   = kit.JumpDockerExec
	JumpSSH          = kit.JumpSSH
	JumpVirshConsole = kit.JumpVirshConsole
)
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 (
	// ResolveNodePath resolves a dotted deployment path against a root map.
	ResolveNodePath = spec.ResolveNodePath
	// SplitDottedPath splits a dotted deployment path into segments (nil on any empty segment).
	SplitDottedPath = spec.SplitDottedPath
	// PathLeaf returns the last (tolerant) segment of a dotted deployment path.
	PathLeaf = spec.PathLeaf
	// ClassifyNodeTarget picks the pod|vm|k8s|local|android target discriminator for a node.
	ClassifyNodeTarget = spec.ClassifyNodeTarget
	// SortedNestedKeys returns a children map's keys in deterministic order.
	SortedNestedKeys = spec.SortedNestedKeys
	// BedCheckLiveRefs returns the ordered `charly check live` targets for a bed.
	BedCheckLiveRefs = spec.BedCheckLiveRefs
	// DescriptionInfo returns the trimmed first line of a candy/box description.
	DescriptionInfo = spec.DescriptionInfo
	// MergeBundleNode overlays src's authored + structural-tree fields onto dst.
	MergeBundleNode = spec.MergeBundleNode
	// HostRooted reports whether a node's stamped descent is a host-rooted (local/SSH-shell) venue.
	HostRooted = spec.HostRooted
	// DeployNestedLocalChildren applies a parent venue's nested target:local children in place.
	DeployNestedLocalChildren = spec.DeployNestedLocalChildren
)

Pure deploy-path helpers (stdlib-only, spec-owned).

View Source
var (
	// CandyMapKey returns a candy's map key (full @github ref for remote, else bare Name).
	CandyMapKey = spec.CandyMapKey
	// CandyStageDirName returns the content-addressed staging dir name (name.version).
	CandyStageDirName = spec.CandyStageDirName
)

Pure candy-stage helpers (spec-owned).

View Source
var (
	// HolderAddrFor derives the resource-arbiter holder address for a deploy-tree node.
	HolderAddrFor = spec.HolderAddrFor
	// FindVMClaimant returns the first node claiming a VM entity via requires_exclusive.
	FindVMClaimant = spec.FindVMClaimant
)

Pure preempt-resolve helpers (spec-owned).

View Source
var (
	// TaskAutoExports are the generator-reserved auto-exported variable names.
	TaskAutoExports = spec.TaskAutoExports
	// TaskKnownNames returns the ${NAME} references that resolve cleanly for a candy.
	TaskKnownNames = spec.TaskKnownNames
	// CandyArtifactRegisters returns the DISTINCT `register:` hints across every candy's artifacts.
	CandyArtifactRegisters = spec.CandyArtifactRegisters
)

Pure task-var + artifact helpers (spec-owned).

View Source
var (
	// RootExecutorForDeployNode selects the ROOT DeployExecutor for a `target: local` node from
	// its `host:` field (ShellExecutor for host:local, SSHExecutor for host:<remote>).
	RootExecutorForDeployNode = specexec.RootExecutorForDeployNode
	// ResolveDeployChain walks a dotted deployment path through the merged tree and returns the
	// leaf node + a composed DeployExecutor chain reaching it.
	ResolveDeployChain = specexec.ResolveDeployChain
	// VmChildExecutor wraps a parent executor with an SSH jump into a VM child node.
	VmChildExecutor = specexec.VmChildExecutor
	// SSHParamsForVm builds an SSHExecutor pointing at a VM's managed ssh-config alias.
	SSHParamsForVm = specexec.SSHParamsForVm
)

deploy_chain.go — the executor-CHAIN constructors moved to spec/exec (the floor primitive home, beside the ShellExecutor/SSHExecutor/NestedExecutor they build — #55 K4); deploykit RE-EXPORTS them here for its plugin-side callers (candy/plugin-bundle, plugin-check, plugin-status, plugin-vm) so those `deploykit.<Fn>` references compile unchanged. charly core calls specexec.<Fn> directly (no deploykit import for the chain). ContainerChain STAYS — it delegates to kit.ContainerChainFromDescriptor (kit-coupled, not a spec/exec pure constructor).

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 (
	QuadletFilename         = spec.QuadletFilename
	QuadletFilenameInstance = spec.QuadletFilenameInstance
	ServiceName             = spec.ServiceName
	ServiceNameInstance     = spec.ServiceNameInstance
)
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 (
	// ResolveTunnelConfig resolves a TunnelYAML into a TunnelConfig with defaults applied.
	ResolveTunnelConfig = spec.ResolveTunnelConfig
	// TunnelConfigFromMetadata creates a TunnelConfig from image-label metadata.
	TunnelConfigFromMetadata = spec.TunnelConfigFromMetadata
)

tunnel_resolve.go — re-export forwarders for the tunnel-config RESOLUTION transforms, now homed in spec (#55 U5 value-type consolidation). ResolveTunnelConfig / TunnelConfigFromMetadata (and their pure parseHostPorts/buildPortMapping/resolveProto helpers) are pure resolve-to-envelope value-transforms over spec value types + spec.ParsePortMapping (delivered by U3) — no live *Config/*Candy graph, no host I/O beyond a stderr diagnostic — so they moved to the spec contract module. deploykit's callers (charly host_build_pod_config_seams, candy/plugin-box, candy/plugin-deploy-pod) reference deploykit.X unchanged. The dead FUNCTIONAL duplicate in sdk/kit/tunnel_metadata.go — a stale copy of these helpers (lacking the live ResolveTunnelConfig; zero production callers) — was retired in the same move (R3 / dead-code).

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 BuilderResolveInputFrom = spec.BuilderResolveInputFrom

BuilderResolveInputFrom builds the serializable spec.BuilderResolveInput a builder plugin's OpResolve leg needs, from the host-computed BuildStageContext. RELOCATED to spec/spec (buildwire_render.go, #55 coneK3tasks) so charly core can call it without an sdk/deploykit import — charly/tasks.go's inline-builder seam now calls spec.BuilderResolveInputFrom directly, shedding the LAST sdk-kit import. This deploykit re-export keeps the candy/plugin- installstep callers (stepEmitBuilder) byte-stable (R3: ONE source in spec/spec, re-exported here) — buildkit.BuilderDef is spec.Builder (alias), so the spec func's *spec.BuilderDef param typechecks against the *buildkit.BuilderDef the callers pass. Shared by the box-build path (resolveDetectionBuilderReply above) AND the pod-overlay build-emit (candy/plugin-installstep stepEmitBuilder) + the INLINE-builder resolve (NewRenderGeneratorFromProject's dg.ResolveInlineBuilder — plugin-side since K-wave 2 cone R1, where it replaced the deleted charly resolveInlineBuilderSeam), R3 — hence the re-export.

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 ExtractMetadata = container.ExtractMetadata

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

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 = container.InspectLabels

InspectLabels reads OCI labels from a local image via engine inspect. Package-level var for testability — re-exported from spec/container (the canonical home). Override container.InspectLabels to stub in tests.

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 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 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 AcquireDeployConfigLock added in v0.2026217.313

func AcquireDeployConfigLock() (func() error, error)

AcquireDeployConfigLock takes the process-shared blocking flock that serializes the read-modify-write of the per-host deploy overlay. Exported because two callers need the lock around a cycle MutateBundleConfig cannot express — a write path that also removes the file (CleanDeployEntry, `charly bundle reset`) and must decide save-vs-remove under the same hold. Every ordinary writer uses MutateBundleConfig instead.

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 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 BringUpMembers added in v0.2026217.313

func BringUpMembers(node *spec.BundleNode, imageTag string) error

BringUpMembers brings up every member of `node` ALONGSIDE the (already-deployed) owner, in deterministic order, on the shared `charly` network. Each member is a folded top-level deploy entry, so bring-up reuses the standard pod pipeline verbatim: the caller persists the member's declared deploy overrides FIRST (so its declared `port:` actually publishes — `charly config` otherwise sources ports from image labels behind an operator -p — see candy/plugin-bundle/members_persist.go and candy/plugin-check's bed-member persist), then this function runs `charly config <member>` + `charly start <member>`, then waits for readiness. A VM member (target: vm) gets the full libvirt lifecycle (create + ssh-wait + deploy), a kind:local member is registered via `charly bundle add <member>`. The SAME helper serves the kind:check bed runner and the operator deploy path (R3). Idempotent on an already-running member.

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 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 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 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 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 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), read func() (*BundleConfig, 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. 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.

read is the current-state re-read this load-mutate-save performs. A nil read falls back to the DeployStateHost-backed LoadBundleConfig — the in-proc host read — INCLUDING the "DeployStateHost==nil → skip" guard (a nil-read caller with no DeployStateHost registered is not compiled to touch the ledger, so the clean is skipped rather than clobbering an unreadable file). A plugin caller (out-of-process command:pod) injects its OWN loader-backed reader (loaderkit.LoadHostBundleConfigViaExecutor), so CleanDeployEntry no longer requires the DeployStateHost package var (#55 coneC-dsh — mirrors the SaveBundleConfig/SaveDeployState reader-callback precedent). The reader is ALSO threaded as SaveBundleConfig's failsafeRead so the data-safety re-check uses the same loader-backed read, not the DeployStateHost-backed one.

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 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 DeployedContainerNames added in v0.2026215.1806

func DeployedContainerNames(dc *BundleConfig) []string

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

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 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 loaderkit.LoadHostBundleConfigViaExecutor (over the executor reverse channel — the former pod-config-load-bundle host seam was retired) 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 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 FillNamespaceBoxViews added in v0.2026213.1748

func FillNamespaceBoxViews(sub *spec.Config, nsLayers map[string]spec.CandyReader, initCfg *spec.InitConfig, child, calver, dir string, opts spec.ResolveOpts, rp *spec.ResolvedProject)

FillNamespaceBoxViews resolves + render-preps every box in a namespace's own config and merges the resulting namespace-QUALIFIED spec.ResolvedBoxView into rp.Boxes (keyed child+"."+name), SKIPPING a box already present (a demand-pulled entry with correctly-requalified cross-refs — never overwrite it, per the RCA'd K1-alpha regression #2). It is the genuine build-engine render-prep DRIVE (buildkit.ResolveBox + RenderPrepBox + ProjectResolvedBox) the namespaced-box fill needs — relocated out of charly core (#55 Cluster-B) because it writes buildkit host-render caches. opts must already carry the project build vocabulary (charly's resolveVocabOpts fills it before calling). Byte-equivalent to the former in-core inner block of the deleted host namespaced-box fill.

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 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.
  • the plugin-side secret resolver (candy/plugin-deploy-pod's buildOverlay → ResolveSecretForCandy → ResolveCandySecret → EnsureCandySecret — the former charly-core ensureCandySecret in layer_secrets.go; #55 coneB-br2 relocated it plugin-side and DELETED 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 GlobalEnvForImage added in v0.2026215.1806

func GlobalEnvForImage(dc *BundleConfig, 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 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 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 InjectInitDependsCandy added in v0.2026215.2321

func InjectInitDependsCandy(cfg *spec.Config, layers map[string]CandyModel, initCfg *spec.InitConfig)

InjectInitDependsCandy prepends each box's ACTIVE init system's `depends_candy` to that box's authored candy list, when the composition actually triggers that init and the dependency candy is not already in THIS BOX'S OWN resolved candy order. Mutates cfg in place; a nil cfg or initCfg is a no-op. It does NOT recurse cfg.Namespaces: a namespace's boxes resolve against the namespace's OWN scanned candy set, so FillNamespaceBoxViews applies the pass there with those layers.

The scope of the presence check is deliberately the box's own order, NOT its base chain: ResolveCandyOrder is called with a nil parentCandies, so a child whose BASE already installs the init candy still has it prepended to the child's list. That is harmless for the artifact — GlobalOrderForBox filters a base-provided candy back out at render, so the emitted image is unchanged, and BoxCandyChain de-duplicates first-occurrence-wins so no collector double-counts it.

The injected candy is PREPENDED, matching how boxes that list the init explicitly already author it (the init candy first, the service candies after) — the topological sort only constrains `require:` edges, so for independent candies the requested order is what survives.

A box whose candy chain does not resolve is SKIPPED rather than erroring: an unresolvable chain is a graph defect the validator (and, on the build path, the subsequent GlobalCandyOrder) reports with far better context than this pass could, and turning it into a resolve abort here would break the error-tolerant validate/inspect projection.

It is IDEMPOTENT, so running it at more than one chokepoint over the same config never double-injects.

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, read func() (*BundleConfig, error)) 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 loads via read. context is a short label threaded to the reader's stderr warning so the caller is identifiable.

read is the per-host-overlay loader (placement-invariant: a plugin caller injects loaderkit.LoadHostBundleConfigViaExecutor; a host in-proc caller may pass a LoadBundleConfig wrapper). #55 coneC-dsh β2+δ seam-death: MergedDeployTree no longer hard-wires LoadDeployConfigForRead (the DeployStateHost-backed read that silently degraded to project-only when DeployStateHost was nil — a placement-dependent correctness regression for the compiled-in arbiter); the reader is injected so the compiled-in candy/plugin-preempt arbiter and the plugin-vm config-resolve Claimant computation load the per-host overlay placement-invariantly. A nil read → no merge (project passes through unchanged) — the semantics the TestMergedDeployTree_ProjectOnlyWhenNoLocalConfig corpus pins.

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 OccupiedHostPorts added in v0.2026215.1806

func OccupiedHostPorts(dc *BundleConfig, 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).

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 PersistBedDeployOverrides added in v0.2026202.1112

func PersistBedDeployOverrides(name string, node BundleNode, externalInPlace bool, marshalNode func(name string, node *BundleNode) (*yaml.Node, error), read func() (*BundleConfig, 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).

read is the current-state re-read this load-mutate-save performs. A nil read falls back to SaveDeployState's own DeployStateHost-backed read (the in-proc host path). A plugin caller (out-of-process command:check / command:bundle) injects its OWN loader-backed reader (loaderkit.LoadHostBundleConfigViaExecutor), so PersistBedDeployOverrides no longer requires the DeployStateHost package var (#55 coneC-dsh — mirrors the SaveBundleConfig/SaveDeployState reader-callback precedent).

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 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, save func(*BundleConfig) error, read func() (*BundleConfig, error)) error

RemoveVmDeployEntry strips deploy.<deployName> from charly.yml. save is the SAME injected node-form persist callback SaveVmDeployState takes — see this file's header — and the load→decide→write runs under the SAME MutateBundleConfig lock hold. read is the SAME reader-callback SaveVmDeployState takes (nil → DeployStateHost-backed LoadBundleConfig; a plugin caller injects its own loader-backed reader).

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 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 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: candy/plugin-pod passes its pluginResolveSecretBackend()/pluginResetCredentialStore/pluginAwaitKeyringUnlock (the keyring-unlock wait RPCs verb:credential `await-unlock` out-of-process in candy/plugin-secrets — core's former resolveSecretBackend/resetDefaultCredentialStore/ awaitKeyringUnlockViaPlugin are DELETED, K-wave 2 cone CONTESTED); 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 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 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), failsafeRead func() (*BundleConfig, 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).

failsafeRead is the config re-read the write path performs for its data-safety guard (below). A nil failsafeRead falls back to LoadBundleConfig — the DeployStateHost-backed host read, so an IN-PROCESS host caller passes nil and behaves exactly as before. A plugin caller that is NOT guaranteed to hold the charly-init DeployStateHost registration (an out-of-process command:bundle) passes its OWN loader-backed reader (loaderkit.LoadHostBundleConfigViaExecutor), so SaveBundleConfig no longer depends on the DeployStateHost package var to re-check the on-disk file. Follows the marshalNode-param precedent: the host-coupled leg is injected, not hard-wired (#55 K4 config-write seam-collapse).

NAMED EXIT for the nil-failsafeRead branch: it is DI serving the still-in-proc host callers (enc_probe / status_flat / vm state, RemoveVmDeployEntry), NOT a transitional shim — the migrated caller (command:bundle) already never passes nil. The nil path dies when the LAST of those host callers migrates plugin-side in its own deferred cone (enc floor / check-cone).

func SaveDeployState added in v0.2026196.928

func SaveDeployState(boxName, instance string, input SaveDeployStateInput, marshalNode func(name string, node *BundleNode) (*yaml.Node, error), read func() (*BundleConfig, 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, save func(*BundleConfig) error, read 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.

The load→modify→save runs inside MutateBundleConfig, so it holds the process-shared blocking deploy-config lock across the whole cycle and re-reads the overlay INSIDE that lock. Without that, 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.

read is the current-state re-read that cycle performs. A nil read falls back to LoadBundleConfig — the DeployStateHost-backed host read — so an IN-PROCESS host caller passes nil and behaves exactly as before. A plugin caller (out-of-process command:vm) injects its OWN loader-backed reader (loaderkit.LoadHostBundleConfigViaExecutor), so SaveVmDeployState no longer requires the DeployStateHost package var (#55 coneC-dsh config-write seam-collapse — mirrors the SaveBundleConfig/SaveDeployState reader-callback precedent).

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 SelectCandiesForPlans added in v0.2026213.1748

func SelectCandiesForPlans(plans []*spec.InstallPlan, candies map[string]spec.CandyReader) []spec.CandyReader

SelectCandiesForPlans returns the candies backing plans — each plan's CandiesIncluded (topo order) then its own Candy — deduped, in first-seen order, resolved against the supplied candies map. A name absent from candies is skipped (the caller's map is authoritative for what exists). Pure: no scan, no I/O — the candies map is whatever the caller already resolved (a host ScanAllCandyWithConfig result, or an envelope's CandyModels).

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 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 SortedKeys added in v0.2026193.1801

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

SortedKeys returns sorted keys from a trie-node map.

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 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 TearDownMembers added in v0.2026217.313

func TearDownMembers(node *spec.BundleNode) error

TearDownMembers tears down every member of `node` in deterministic order — the companion to BringUpMembers. It attempts every member and returns their joined errors so callers can finish the full cleanup while still failing the owning operation.

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 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 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 = spec.BuilderPreresolved

BuilderPreresolved is the pre-resolved builder-context payload — the TYPE now lives in spec (#55 value-type consolidation; a plain in-process value carrier, not a wire type: candy/plugin-bundle's preresolveBuilderContexts builds it plugin-side over exec.InvokeProvider and the pure compiler only reads it, so it never crosses the process boundary — HostContext.BuilderContext is populated in-proc AFTER the HostContextJSON decode). This forwarder keeps deploykit's callers + candy/plugin-bundle compiling unchanged. The externalized-builder WORD SET itself needs no new sharing mechanism — it rides the wire as spec.ResolvedProject.ExternalizedBuilders.

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 = spec.BundleConfig

--- BundleConfig state container (moved from charly/deploy.go, P4) --- BundleConfig represents per-machine deployment overrides (~/.config/charly/charly.yml). SPIKE (value-type relocation, #55 cluster 2): relocated to spec.BundleConfig (spec/spec/bundle_config.go) — every field already resolved to a spec.* type, so the type carried zero deploykit-only content. This is now a zero-churn alias; the two pure methods (Lookup/LookupKey) moved with it. The three methods that reach sdk/kit (DeployedContainerNames/OccupiedHostPorts/GlobalEnvForImage) stay below as free functions (bundle_derive.go) — spec can never import sdk/kit.

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 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 MutateBundleConfig added in v0.2026217.313

func MutateBundleConfig(read func() (*BundleConfig, error), save func(dc *BundleConfig) error, mutate BundleConfigMutator) (*BundleConfig, error)

MutateBundleConfig runs ONE locked read-modify-write cycle over the per-host deploy overlay and returns the fresh config the mutation ran against, so an in-memory caller can adopt it as its new view instead of continuing on its stale snapshot.

read is the caller's overlay reader (a plugin passes its loader-backed loaderkit.LoadHostBundleConfigViaExecutor; an in-proc host caller passes LoadBundleConfig). save is the caller's persist callback (a SaveBundleConfig closure carrying that caller's node-form marshal). Both are injected for the SAME reason every other write path injects them: the deploy-kind-specific marshal and the placement-specific read are the caller's responsibility, and this shell stays kind-blind.

The lock is BLOCKING: a config write is brief, so a concurrent writer waits rather than failing.

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.

type BundleConfigMutator added in v0.2026217.313

type BundleConfigMutator func(dc *BundleConfig) (changed bool, err error)

BundleConfigMutator mutates a FRESH BundleConfig read under the deploy-config lock. It reports whether it changed anything: false skips the write entirely (so a no-op decision costs a read, not a whole-file rewrite). dc is never nil and dc.Bundle is never nil — MutateBundleConfig self-heals both before calling, so a mutation writes `dc.Bundle[key] = entry` unconditionally.

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.

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.

func CredentialAccessViaExecutor added in v0.2026213.1748

func CredentialAccessViaExecutor(ctx context.Context, ex *sdk.Executor) CredentialAccess

CredentialAccessViaExecutor bundles verb:credential's resolve+set ops into a CredentialAccess, driven over the given sdk.Executor's InvokeProvider (peer-dispatch to candy/plugin-secrets). Use it anywhere a deploy-time plugin needs to resolve candy secret_requires:/secret_accepts: or an enc passphrase (ResolveSecretForCandy / ResolveEncPassphrase / 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 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.

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 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 *spec.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 — mode "bytes".
	// NewRenderGeneratorFromProject wires this to renderSeamCaller.validateEgress
	// (render_generator_from_project.go), a direct verb:egress InvokeProvider peer
	// dispatch — verb:egress is compiled-in, no host round-trip needed.
	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 — mode
	// "text". Distinct from ValidateEgress (bytes) because the Containerfile is a rendered
	// string, not a YAML/JSON blob (#67 render-DRIVE move). Same wiring/dispatch mechanism
	// as ValidateEgress above, just the "text" mode.
	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. NewRenderGeneratorFromProject wires
	// this to renderSeamCaller.renderService (render_generator_from_project.go) — direct
	// kind:init + verb:egress InvokeProvider peer dispatch, no host round-trip (both are
	// compiled-in). The former charly-core RenderService this comment used to describe is
	// DELETED (#55 W3 B4) — the core deploy-time render-service HostBuild seam is gone too;
	// compile_service_steps.go's renderServiceViaSeam reuses THIS SAME
	// renderSeamCaller.renderService now, so both the build-time (this field) and
	// deploy-time paths share one implementation. 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 OpResolves an externalized INLINE builder and returns its in-candy
	// fragment (C10 InlineFragment) — plugin-side peer-dispatch since K-wave 2 cone R1, sharing the
	// ONE resolveBuilderStage its detection/external siblings below use (R3). The builder plugin is
	// connected on demand by that Invoke itself (ops.InvokeProviderOpts.ExtraRef), which is why no
	// separate EnsureBuildersConnected seam sits beside it any more. Used by WriteCandySteps.
	ResolveInlineBuilder func(candyName, builderName string, bDef *buildkit.BuilderDef, ctx *spec.BuildStageContext, img *spec.ResolvedBox) (inlineFragment string, err 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 *spec.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 *spec.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 calls
	// deploykit.EmitBakedPlugins directly (the former HostBuild("bake-plugins") is DELETED).
	// 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 by the resolve Invoke itself (ops.InvokeProviderOpts.ExtraRef — K-wave 2 cone R1 retired the separate EnsureBuildersConnected pre-pass, whose two-pass scan+load was a second copy of the host's own connectPluginByWordRef). 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 = spec.HostContext

HostContext is the deploy-compile host-context value carrier — the TYPE now lives in spec (spec.HostContext, #55 K4 import-purity; a hand-written spec value type, NOT a CUE wire type: it crosses ONLY as the opaque `host_context: bytes` RawBody, and its BuilderContext/ActiveInit/ ActiveInitName are json:"-" in-process fields the plugin populates after decode — the gengotypes spike cannot express that json:"-" intent, so hand-written is SPIKE-JUSTIFIED). This forwarder keeps the compile mechanism (BuildDeployPlan + the compile helpers) + candy/plugin-bundle compiling unchanged; charly core constructs spec.HostContext directly.

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 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("step-emit",
	// {Word:"oci-emit-step"})) uses to look up the host-side cached overlay buildEngineContext (the
	// host overlay-prep constructs + caches it per dir). 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 in its OverlayBuildReply envelope;
	// 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("step-emit", {Word:"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

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