spec

package
v0.2026248.128 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Pure structural methods on the generated/aliased spec types, relocated here from package main by the CUE-single-source cutover (WF-B THE REPOINT). A method on an aliased type (e.g. type BoxConfig = spec.Box in package main) cannot be declared in package main — the type is foreign — so every method that touches ONLY the type's own fields + the stdlib lives here, beside the type it acts on. Methods that reach package-main internals (VerbCatalog, Phase/Venue, EmitOpts, the readiness/preempt consts, …) stay in package main as free functions taking a *spec.X — they are NOT here. STANDALONE: this package imports nothing from package main.

Charly-name aliases for the generated `spec` types.

WF-B repoints package main onto this package via zero-churn `type <CharlyName> = spec.<CharlyName>` aliases. For that to compile the spec package must EXPOSE every param type under its charly name. The DEFMAP names the (cue-def → charly) mapping.

MECHANISM (RDD finding): `cue exp gengotypes` v0.16.1 does NOT propagate a def-level `@go(CharlyName)` rename to the FIELDS that reference that def — the referencing field keeps the original def name and the emitted Go dangles (uncompilable). Verified on a live spike. So the charly NAME is exposed here as a Go type alias (`type BoxConfig = Box`) instead of via a def-level attribute: identical drop-in semantics (the alias and the generated type are the SAME type), zero generated-file churn, and no broken references. The field-level `@go(GoName,…)` attributes (which DO work) carry the per-field name + pointer + type overrides in schema/*.cue.

HAND-WRITTEN — not emitted by `task cue:gen`; the reproducibility gate (gen_repro_test.go) only covers cue_types_gen.go + vocab_gen.go.

Hand-written types for shapes `cue exp gengotypes` cannot express faithfully:

  • PortSpec — the candy `port:` element is a (int | "proto:port") disjunction the Go normalizer canonicalizes to {port, protocol}; gengotypes degrades the disjunction to `any`.
  • VmDeployState — MACHINE-WRITTEN runtime deploy state. Its CUE def carries an open `...` tail (forward-evolving state record), which makes gengotypes degrade the whole def to `map[string]any`. The hand Go field is a CONCRETE struct, so #VmDeployState is @go(-)'d and the faithful struct (+ its nested state sub-types) is mirrored HERE. Kept in lockstep with the CUE def (schema/deploy.cue #VmDeployState) — every field here has a matching CUE field (e.g. PortForwards ⇄ port_forwards, the auto-forward record).

Ported from the package-main param structs (the source of truth); keep in lockstep until WF-B repoints package main onto this package. STANDALONE — no package-main symbol is referenced.

Scalar collapses for the CUE-single-source cutover (WF-B THE REPOINT).

CUE validates these named scalars (CalVer/EntityRef/CandyRef pattern strings, VmSize/Size/PortPin/Duration patterns, the 11 live-verb method enums, the build-format/-type enums) at load — so the Go side gains nothing from a DISTINCT named string type, and package-main code uses plain string/[]string throughout. The matching CUE defs are `@go(-)` (no generated named type); the referencing fields keep the def name, which these Go ALIASES resolve to the builtin. An alias (not a defined type) is load-bearing: `&op.Cdp` is a `*string`, `[]PortPin` IS `[]string`, and an untyped/string assignment to a CalVer/EntityRef field just works — exactly the charly drop-in semantics.

MECHANISM (RDD finding, mirrors spec/charly_names.go): `cue exp gengotypes` v0.16.1 does NOT propagate a def-level rename/suppression to the FIELDS that reference the def, so a bare `@go(-)` leaves the field dangling on the deleted name. Exposing the name as a Go alias here makes every reference compile while collapsing the Go type to the builtin.

HAND-WRITTEN — not emitted by `task cue:gen`.

Hand-written union / shorthand types for the generated `spec` package.

`cue exp gengotypes` degrades every CUE disjunction to `any` / `map[string]any` / an empty struct (the Go type system cannot express a disjunction). For the handful of schema defs whose Go shape charly relies on, the matching CUE def is annotated `@go(-)` in schema/*.cue (suppressing the lossy generated type) and the faithful type is hand-written HERE, in the SAME package, so the generated structs reference these by name. These are ported from the hand-written charly param types (the source of truth named in each comment); keep them in lockstep until WF-B repoints package main onto this package.

STANDALONE: this package builds on its own (`go build ./spec/...`) and reaches into NO package-main symbol — every method here is self-contained.

vm_box_labels.go — the VM-box metadata label contract (cutover plan task 2: the generic VM box). spec.VmBoxMetadata rides ONE OCI label — spec.LabelVmBox (ai.opencharly.vm.box) — whole-struct-marshaled as JSON, the VM analog of the per-field ai.opencharly.* label family the pod side names via deploykit's CapabilityLabelMap. Every exported field of VmBoxMetadata MUST have a VmBoxLabelMap entry: the map is the struct ↔ label sync table proving the WHOLE contract rides the one label, so a future source-less VM deploy (`charly fleet from-box vm:<ref>` / VmCapabilitiesFromLabels) can reconstruct every field from a pushed box image. Maintained alongside #VmBoxMetadata (CUE-sourced, schema/vm_box_metadata.cue) — adding a field to spec.VmBoxMetadata without an entry here trips CheckVmBoxLabelCompleteness and breaks the build.

Index

Constants

View Source
const (
	// PreemptStopShutdown is the only supported stop mechanism (graceful ACPI shutdown / podman
	// stop; disk preserved).
	PreemptStopShutdown = "shutdown"
	// PreemptRestoreAlways restarts the holder regardless of the claim's outcome (the default).
	PreemptRestoreAlways = "always"
	// PreemptRestoreSuccess restarts the holder only if the claim released cleanly; on a failed
	// claim it is left stopped for operator inspection.
	PreemptRestoreSuccess = "on-success"
)
View Source
const (
	ArbiterActionAcquireExclusive = "acquire-exclusive"
	ArbiterActionAcquireShared    = "acquire-shared"
	ArbiterActionRelease          = "release-claimant"
	ArbiterActionStatus           = "status"
	ArbiterActionReconcile        = "reconcile-stranded"
	ArbiterActionClearPoison      = "clear-poison"
	ArbiterActionResourcePoisoned = "resource-poisoned"
)
View Source
const (
	CheckLevelNone    = "none"
	CheckLevelBuild   = "build"
	CheckLevelNoAgent = "noagent"
	CheckLevelAgent   = "agent"
)

The per-box acceptance-depth rungs.

View Source
const (
	EncMethodMount   = "mount"   // mount the not-yet-mounted volumes (init must already exist)
	EncMethodUnmount = "unmount" // fusermount3 -u + stop the scope unit
	EncMethodEnsure  = "ensure"  // auto-init then mount (charly start transparent setup)
	EncMethodPasswd  = "passwd"  // gocryptfs -passwd for every initialized volume
)

EncMethod selects which gocryptfs operation plugin-enc runs.

View Source
const (
	// GpuModeVfio — EVERY function of the GPU's IOMMU group bound to vfio-pci
	// (free for VM passthrough; the boot default on a passthrough host).
	GpuModeVfio = "vfio"
	// GpuModeNvidia — each function on its correct host driver (display -> nvidia
	// for CDI-shared pods, HDMI-audio -> snd_hda_intel).
	GpuModeNvidia = "nvidia"

	// NvidiaVendorID is the normalized PCI vendor of NVIDIA cards (the device_lock
	// wedge is an nvidia-driver concept).
	NvidiaVendorID = "0x10de"

	// HostDriverDisplay / HostDriverAudio / HostDriverVfio are the host drivers a
	// group function binds to: the display (VGA/3D, class 0x03xx) takes nvidia in
	// host mode; the HDMI-audio (class 0x0403) takes snd_hda_intel; everything takes
	// vfio-pci for passthrough.
	HostDriverDisplay = "nvidia"
	HostDriverAudio   = "snd_hda_intel"
	HostDriverVfio    = "vfio-pci"
)
View Source
const (
	GpuSwitchActionMode          = "switch-mode"     // GpuSwitchInput{Gpu?,Vendor?,Mode} -> flip the group's driver mode (Gpu set = exact card; Vendor set = tolerant vendor-select)
	GpuSwitchActionEnsureCDI     = "ensure-cdi-root" // regenerate /etc/cdi/nvidia.yaml as ROOT (distinct from the C11 user-scope "ensure-cdi" detection action)
	GpuSwitchActionWedgeDetected = "wedge-detected"  // read-only D-state probe -> Bool
	GpuSwitchActionGroupInMode   = "group-in-mode"   // GpuSwitchInput{Gpu,Mode} -> Bool
	GpuSwitchActionCurrentMode   = "current-mode"    // GpuSwitchInput{Gpu} -> Str (vfio|nvidia)
	GpuSwitchActionDisplayDriver = "display-driver"  // GpuSwitchInput{Addr} -> Str (live driver of a PCI function)
	GpuSwitchActionPlan          = "switch-plan"     // GpuSwitchInput{Gpu,Mode} -> Plan (DRY-RUN commands; NO sysfs write) — the C9 cred/hw-free dispatch proof
)

GPU driver-switch actions served by verb:gpu (cutover C9).

View Source
const (
	LabelVersion          = "ai.opencharly.version"
	LabelBox              = "ai.opencharly.box"
	LabelRegistry         = "ai.opencharly.registry"
	LabelBootc            = "ai.opencharly.bootc"
	LabelUID              = "ai.opencharly.uid"
	LabelGID              = "ai.opencharly.gid"
	LabelUser             = "ai.opencharly.user"
	LabelHome             = "ai.opencharly.home"
	LabelPort             = "ai.opencharly.port"
	LabelVolume           = "ai.opencharly.volume"
	LabelAlias            = "ai.opencharly.alias"
	LabelSecurity         = "ai.opencharly.security"
	LabelNetwork          = "ai.opencharly.network"
	LabelEnv              = "ai.opencharly.env"
	LabelHook             = "ai.opencharly.hook"
	LabelRoute            = "ai.opencharly.route"
	LabelInit             = "ai.opencharly.init"
	LabelInitDef          = "ai.opencharly.init_def"
	LabelEnvCandy         = "ai.opencharly.env_candy"
	LabelPathAppend       = "ai.opencharly.path_append"
	LabelPortProto        = "ai.opencharly.port_proto"
	LabelPortRelay        = "ai.opencharly.port_relay"
	LabelSkill            = "ai.opencharly.skill"
	LabelStatus           = "ai.opencharly.status"
	LabelInfo             = "ai.opencharly.info"
	LabelCandyVersion     = "ai.opencharly.candy_version"
	LabelSecret           = "ai.opencharly.secret"
	LabelPlatformDistro   = "ai.opencharly.platform.distro"
	LabelPlatformFormat   = "ai.opencharly.platform.format"
	LabelBuilderUse       = "ai.opencharly.builder.use"
	LabelBuilderProvide   = "ai.opencharly.builder.provide"
	LabelDataEntries      = "ai.opencharly.data"
	LabelDataBox          = "ai.opencharly.data_box"
	LabelEnvProvide       = "ai.opencharly.env_provide"
	LabelEnvRequire       = "ai.opencharly.env_require"
	LabelEnvAccept        = "ai.opencharly.env_accept"
	LabelSecretAccept     = "ai.opencharly.secret_accept"  // credential-store-backed env vars this image can optionally use
	LabelSecretRequire    = "ai.opencharly.secret_require" // credential-store-backed env vars this image must have
	LabelMCPProvide       = "ai.opencharly.mcp_provide"
	LabelAgentProvide     = "ai.opencharly.agent_provide"
	LabelTerminalProfiles = "ai.opencharly.terminal_profiles"
	LabelMCPRequire       = "ai.opencharly.mcp_require"
	LabelMCPAccept        = "ai.opencharly.mcp_accept"
	LabelDescription      = "ai.opencharly.description"
	LabelService          = "ai.opencharly.service"
	LabelShell            = "ai.opencharly.shell"
	LabelCheckLevel       = "ai.opencharly.check_level"

	// VM-box metadata contract (the cutover task 2 label): the WHOLE spec.VmBoxMetadata
	// marshaled as one JSON label, like LabelService does for CapabilityService. A NEW
	// label with no byte-freeze anchor — its json tags (CUE-sourced, vm_box_metadata.cue)
	// ARE the wire contract.
	LabelVmBox = "ai.opencharly.vm.box"
)
View Source
const (
	DefaultBoxDir   = "box"   // discovered box/<name>/ directory
	DefaultCandyDir = "candy" // discovered candy/<name>/ directory
)

layout_paths.go — the stable per-directory discovery path constants (the discovered box/<name>/ and candy/<name>/ dirs). Relocated from sdk/kit/migrate_support.go (#55 import-purity cone-render): plain loader-result DATA the types-only spec module owns, mirroring spec.UnifiedFileName. kit re-exports them via const alias so existing kit.DefaultBoxDir / kit.DefaultCandyDir call sites (plugins + sdk) are untouched.

View Source
const (
	ProjectDirEnv  = "CHARLY_PROJECT_DIR"
	ProjectRepoEnv = "CHARLY_PROJECT_REPO"
)

ProjectDirEnv / ProjectRepoEnv are the environment names that carry charly's PROJECT SCOPE — the two mutually-exclusive answers to "which project's charly.yml is this process operating on". Canonical loader DATA, sharing the home of the manifest filename they select: the charly CLI publishes them as the `-C/--dir` and `--repo` flag environments, and every module that spawns a charly child (sdk/deploykit's packaging child) must set or strip the SAME two names, so the contract lives in the one module both sides import rather than as a literal in each.

They are the DESTINATION, not yet the whole story: literal spellings still exist in the charly CLI (charly/plugin_command_prescan.go, charly/main_dir_test.go, charly/main_repo_test.go, charly/plugin_command_prescan_repo_test.go) and are converted alongside their consumers in opencharly/charly#309. The Kong struct tags in charly/main.go are the one PERMANENT exception: a Go struct tag is a string literal in the type declaration and cannot reference a constant.

View Source
const DefaultCheckLevel = CheckLevelNoAgent

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

View Source
const DefaultProjectRepo = "github.com/opencharly/charly"

DefaultProjectRepo is the repo --repo defaults to when the spec is the literal string "default" (or when charly mcp serve auto-falls back).

View Source
const DeployConfigEnv = "CHARLY_DEPLOY_CONFIG"

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

View Source
const ExternalStepKindPrefix = "external:"

ExternalStepKindPrefix marks a StepKind string as an external (plugin-contributed) kind.

View Source
const HomeToken = "{{.Home}}"

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

View Source
const LedgerSchemaVersion = "2026.161.1649"

ledger_records.go — the install-ledger record shapes relocated from sdk/kit/install_ledger.go + sdk/kit/migrate_support.go (#55 value-type relocation spike, cluster 4) and CUE-sourced at schema/deploy.cue (generated into cue_types_gen.go — DeployRecord/CandyRecord/StepRecord are wire shapes, JSON-persisted to the ledger). kit.* becomes a type/const alias onto these. This file holds only the ledger-format version constant.

LedgerSchemaVersion is the install-ledger record format version (the ledger-candy-keys cutover's CalVer). It is INDEPENDENT of the project schema CalVer (spec/calver.LatestSchemaCalVer) — a non-ledger schema cutover that bumps the project HEAD must NOT invalidate the ledger gate.

View Source
const ReverseOpPluginScriptKey = "script"

ReverseOpPluginScriptKey is the Extra map key carrying a ReverseOpPluginScript's shell-script body. Exported so both the host handler and the SDK builder name the one key (R3 — no magic-string drift across the process boundary).

View Source
const SchemaFloor = "2026.174.1100"

SchemaFloor is the oldest schema version `charly migrate` can migrate FROM (schema/version.cue #SchemaFloor).

View Source
const SchemaVersion = "2026.240.1943"

SchemaVersion is the HEAD schema CalVer (schema/version.cue #SchemaVersion).

View Source
const UnifiedFileName = "charly.yml"

UnifiedFileName is the ONE box/candy manifest filename. Canonical loader DATA; ScanSpec below defaults to it. (sdk/kit keeps the sibling layout constants DefaultBoxDir/DefaultCandyDir.)

Variables

AllStepKinds is the fixed InstallStep IR vocabulary — the 14 compiled-in concrete step kinds (external:<word> kinds are dynamic and not listed).

View Source
var AuthoringVerbs = []string{
	"build",
	"cache",
	"caps",
	"comment",
	"content",
	"context",
	"copy",
	"depends_on",
	"description",
	"download",
	"env",
	"eventually",
	"exclude_distro",
	"exit_status",
	"extract",
	"extract_include",
	"id",
	"link",
	"mkdir",
	"mode",
	"retry_interval",
	"run_as",
	"setcap",
	"skip",
	"stderr",
	"stdout",
	"strip_components",
	"tag",
	"target",
	"timeout",
	"to",
	"uninstall",
	"unless_exists",
	"write",
}

AuthoringVerbs is the AUTHORABLE #Op field vocabulary (#Op fields minus the never-authored origin/venue/intent_do/plugin/plugin_input/command).

View Source
var ContextWords = []string{
	"build",
	"deploy",
	"runtime",
}

ContextWords is the plan-step execution contexts (#Context).

View Source
var DistroFormats = map[string]string{
	"almalinux":   "rpm",
	"alpine":      "apk",
	"arch":        "pac",
	"archarm":     "pac",
	"cachyos":     "pac",
	"centos":      "rpm",
	"debian":      "deb",
	"endeavouros": "pac",
	"fedora":      "rpm",
	"manjaro":     "pac",
	"omarchy":     "pac",
	"rhel":        "rpm",
	"rocky":       "rpm",
	"ubuntu":      "deb",
}

DistroFormats is each distro id's native package format (#Distros[id].format). The SINGLE table — hostenv.FormatForDistroID reads it; there is no hand-written Go copy.

View Source
var DistroIDs = []string{
	"almalinux",
	"alpine",
	"arch",
	"archarm",
	"cachyos",
	"centos",
	"debian",
	"endeavouros",
	"fedora",
	"manjaro",
	"omarchy",
	"rhel",
	"rocky",
	"ubuntu",
}

DistroIDs is the CLOSED guest-distro id vocabulary, derived from #Distros' own keys (schema/distro_vocab.cue).

View Source
var DistroInits = map[string]string{
	"almalinux":   "systemd",
	"alpine":      "openrc",
	"arch":        "systemd",
	"archarm":     "systemd",
	"cachyos":     "systemd",
	"centos":      "systemd",
	"debian":      "systemd",
	"endeavouros": "systemd",
	"fedora":      "systemd",
	"manjaro":     "systemd",
	"omarchy":     "systemd",
	"rhel":        "systemd",
	"rocky":       "systemd",
	"ubuntu":      "systemd",
}

DistroInits is each distro id's guest init system (#Distros[id].init) — systemd or openrc. Replaces `if distro == "alpine"` branches.

View Source
var DistroOvmfFamilies = map[string]string{
	"almalinux":   "fedora",
	"arch":        "arch",
	"cachyos":     "arch",
	"centos":      "fedora",
	"debian":      "debian",
	"endeavouros": "arch",
	"fedora":      "fedora",
	"manjaro":     "arch",
	"omarchy":     "arch",
	"rhel":        "fedora",
	"rocky":       "fedora",
	"ubuntu":      "debian",
}

DistroOvmfFamilies is each distro id's OVMF firmware family (#Distros[id].ovmf_family), for the ids that declare one. Replaces the hand-maintained ovmf_distro_aliases: map that was kept in two byte-identical copies. An id with no entry keeps the all-families fallback.

View Source
var DistroSSHUnits = map[string]string{
	"almalinux":   "sshd",
	"alpine":      "sshd",
	"arch":        "sshd",
	"archarm":     "sshd",
	"cachyos":     "sshd",
	"centos":      "sshd",
	"debian":      "ssh",
	"endeavouros": "sshd",
	"fedora":      "sshd",
	"manjaro":     "sshd",
	"omarchy":     "sshd",
	"rhel":        "sshd",
	"rocky":       "sshd",
	"ubuntu":      "ssh",
}

DistroSSHUnits is each distro id's OpenSSH service name (#Distros[id].ssh_unit) — `ssh` on Debian-family, `sshd` elsewhere. Replaces a hardcoded Go switch.

View Source
var DocDirectives = []string{
	"cache",
	"compiled_plugins",
	"context_ignore_baseline",
	"defaults",
	"discover",
	"import",
	"ledger",
	"ovmf_distro_aliases",
	"ovmf_paths",
	"providers",
	"provides",
	"repo",
	"system",
	"version",
}

DocDirectives is the reserved document directives (#NodeDoc top-level keys).

View Source
var ErrGPUSwitchWedged = errors.New("GPU driver switch wedged: nvidia .remove stuck holding the device_lock — host reboot required")

ErrGPUSwitchWedged signals that a GPU driver switch did not complete because the nvidia `.remove` is stuck holding the device_lock (deadline exceeded, or a self-detected D-state task in nv_pci_remove). candy/plugin-gpu carries the wedge as a bool over the wire (GpuSwitchReply.Wedged); the core gpu shim maps that bool back to this sentinel so callers (vm_gpu_cmd) keep matching it with errors.Is. Recovery is a host reboot.

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

ErrImageNotLocal is returned (wrapped with the image ref) when an image is not present in local container storage — a host-side signal, never crossing the plugin wire.

View Source
var ErrNotSupported = errors.New("live-stdio (interactive/stream) not supported on this venue")

ErrNotSupported is returned by RunInteractive/RunStream on a venue that cannot host a LIVE-STDIO session (a capture-only / non-interactive executor). Callers surface it as "shell/logs not supported on this substrate" — an explicit, compile-forced failure, never a silent no-op (F12; the honest form of the executor-matrix ErrNotSupported path).

View Source
var ErrStaleLocalImage = errors.New("short name resolves to a stale local image")

ErrStaleLocalImage is returned (wrapped with both refs) when a short-name resolve elects a local image that is NOT the newest build of that box, for a verb that pronounces a verdict on the artifact. Certifying an artifact older than the one on disk produces a green result that proves nothing, so the resolution is refused rather than silently taken (container.ResolveBuiltImageRef).

View Source
var ExternalizedBuilders = map[string]bool{
	"cargo": true,
	"npm":   true,
	"pixi":  true,
	"aur":   true,
	"mise":  true,
}

ExternalizedBuilders is THE single source of truth for which builder words are served by an EXTERNAL out-of-process plugin. A word here resolves through the registry to a *grpcProvider connected at plugin-load time. mise: the builder:mise plugin (opencharly/plugin-mise) — externalized like the four detection builders; the embedded builder: mise: vocabulary in charly selects it by detect_file (mise.toml/.tool-versions).

View Source
var InstallVerbs = map[string]bool{
	"mkdir": true, "copy": true, "write": true, "link": true,
	"download": true, "setcap": true, "build": true,
}

InstallVerbs are the verbs that render directly to a generic OpStep install step (a Containerfile directive at build, a deploy shell command at deploy). The verbs that lowered into a TYPED install step (package/service) are now extracted plugin verbs whose TypedStepProvider owns the lowering — handled by charly core's opActsInBuildDeploy, not this map.

View Source
var KindValueDefs = map[string]string{
	"android":    "#AndroidValue",
	"candy":      "#CandyValue",
	"kubernetes": "#KubernetesValue",
	"local":      "#LocalValue",
	"pod":        "#PodValue",
	"vm":         "#VmValue",
}

KindValueDefs is the word→#<Kind>Value CUE-def map the host uses to closedness-gate a substrate/candy node's authored VALUE (validateKindValueCUE). DERIVED from the #<X>Value defs themselves (every one except the shared #DeployValue disjunct), so a new value-gated kind needs no hand-maintained map.

View Source
var KindWords = []string{}

KindWords is the reserved kind keywords (the #Node disjunction discriminators).

View Source
var OpFields = []string{
	"build",
	"cache",
	"caps",
	"command",
	"comment",
	"content",
	"context",
	"copy",
	"depends_on",
	"description",
	"download",
	"env",
	"eventually",
	"exclude_distro",
	"exit_status",
	"extract",
	"extract_include",
	"id",
	"intent_do",
	"link",
	"mkdir",
	"mode",
	"origin",
	"plugin",
	"plugin_input",
	"retry_interval",
	"run_as",
	"setcap",
	"skip",
	"stderr",
	"stdout",
	"strip_components",
	"tag",
	"target",
	"timeout",
	"to",
	"uninstall",
	"unless_exists",
	"venue",
	"write",
}

OpFields is every #Op verb/modifier field name (the flat Op vocabulary).

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

OpInContext reports whether an op runs in the given exec context. Its fallback consults the kernel VerbCatalog (charly), so charly injects the impl at init.

View Source
var OpVerbs = []string{
	"mkdir",
	"copy",
	"write",
	"link",
	"download",
	"setcap",
	"build",
	"plugin",
}

OpVerbs is the verb DISCRIMINATOR vocabulary (#OpVerb) — the exactly-one-set verb subset of #Op fields (Op.Kind() + the VerbCatalog dispatch table gate against it).

View Source
var PluginEmitStepWords = map[StepKind]string{
	StepKindFile:            "file",
	StepKindShellHook:       "shell-hook",
	StepKindShellSnippet:    "shell-snippet",
	StepKindServicePackaged: "service-packaged",
	StepKindServiceCustom:   "service-custom",
	StepKindRepoChange:      "repo-change",
	StepKindApkInstall:      "apk-install",
	StepKindReboot:          "reboot",
	StepKindSystemPackages:  "system-packages",
	StepKindBuilder:         "builder",
	StepKindLocalPkgInstall: "local-pkg-install",
	StepKindOp:              "op",
	StepKindExtract:         "extract",
}

PluginEmitStepWords maps a builtin InstallStep kind to the lowercase-hyphenated class:step plugin word that serves its pod-overlay OpEmit (candy/plugin-installstep). These kinds have NO in-proc StepProvider — the OCI dispatch routes them here, serializing the step VIEW as the OpEmit payload. Their DEPLOY leg is unchanged (sdk/kit.WalkPlans renders them from the same view; reboot's is the host-side guest reboot over RunHostStep). apk-install's and reboot's plugin declares Emits=false (no build fragment); every other word Emits=true.

Shared cross-boundary (R3): BOTH the host (charly/provider_step.go's checkStepProviderBijection) and the plugin itself (candy/plugin-installstep's "oci-dispatch" word) consult the SAME map — it belongs with the fixed step-kind vocabulary (AllStepKinds) in the IR envelope.

View Source
var ResourceKinds = []string{
	"pod",
	"vm",
	"kubernetes",
	"local",
	"android",
	"group",
}

ResourceKinds is the DEPLOYABLE subset of the kind keywords — the kinds whose #Node arm nests a sub-ENTITY (resource) child (#ResourceKind).

View Source
var StepKeywords = []string{
	"agent-check",
	"agent-run",
	"check",
	"include",
	"run",
}

StepKeywords is the plan-step intent keywords (#Step arms minus #Op fields).

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

ValidateRecord is the egress-validation seam for install-ledger writes. The ledger validates each record against its egress schema before writing; charly injects its ValidateEgressValue here at init. Defaults to a no-op for standalone use.

View Source
var VerbCatalog = map[string]VerbSpec{

	"mkdir":    {ctxBuildDeploy, DoAct, false},
	"copy":     {ctxBuildDeploy, DoAct, true},
	"write":    {ctxBuildDeploy, DoAct, true},
	"link":     {ctxBuildDeploy, DoAct, true},
	"download": {ctxBuildDeploy, DoAct, true},
	"setcap":   {ctxBuildDeploy, DoAct, false},
	"build":    {ctxBuildDeploy, DoAct, false},

	"plugin": {ctxBuildDeployRuntime, DoAssert, false},
}

VerbCatalog is the single source of truth for every core install-verb's legality, default do-mode, and reversibility — one table driving validation, dispatch, and lowering. Keys match spec.OpVerbs (gated by the registry bijection in charly/reserved_registry.go).

View Source
var VmBoxLabelMap = map[string]string{

	"Distro":      LabelVmBox,
	"Arch":        LabelVmBox,
	"Firmware":    LabelVmBox,
	"Init":        LabelVmBox,
	"Version":     LabelVmBox,
	"Description": LabelVmBox,

	"BaseUser":      LabelVmBox,
	"SSHUser":       LabelVmBox,
	"CharlyInstall": LabelVmBox,

	"Source": LabelVmBox,
	"Plan":   LabelVmBox,
}

VmBoxLabelMap names the OCI label each spec.VmBoxMetadata field rides. All fields share the single whole-struct label (LabelVmBox) — the values are uniform by design; the map's job is the completeness gate, not per-field routing.

Functions

func BareCandyRef

func BareCandyRef(ref string) string

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

func BoxNamesOf

func BoxNamesOf(m BoxMap) []string

BoxNamesOf returns the image names in a generic image map, sorted for determinism.

func BuilderPhaseTemplate added in v0.2026213.1726

func BuilderPhaseTemplate(b *Builder, phase Phase, venue Venue) string

BuilderPhaseTemplate is the Builder analog of FormatPhaseTemplate. The phase: block is the single source of truth — the legacy inline InstallTemplate field was removed (R5) and its content migrated into phase.install.container, so there is no fallback arm. Multi-stage builders render via their plugin's OpResolve (kit.BuilderResolve), NOT this resolver.

func CandyCapabilitiesError added in v0.2026213.1726

func CandyCapabilitiesError(layers map[string]CandyReader, order []string, missing []string) error

CandyCapabilitiesError formats a missing-capabilities error with a remediation hint pointing at which candies requested what.

func CheckLevelReaches added in v0.2026213.1726

func CheckLevelReaches(have, want string) bool

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

func CheckRequiredCapabilities added in v0.2026213.1726

func CheckRequiredCapabilities(layers map[string]CandyReader, order []string, agg *AggregatedCandyCaps) []string

CheckRequiredCapabilities returns a sorted list of capability names requested via `requires_capabilities:` on any candy in `order` but not provided by the aggregated capabilities. Empty slice on success.

func CheckVmBoxLabelCompleteness added in v0.2026246.554

func CheckVmBoxLabelCompleteness() error

CheckVmBoxLabelCompleteness returns an error listing any spec.VmBoxMetadata exported field that lacks a VmBoxLabelMap entry. Called from TestVmBoxLabelCompleteness to fail the build when a field is added without a label mapping.

func ChildProjectEnv added in v0.2026230.1336

func ChildProjectEnv(inherited []string, projectDir string) []string

ChildProjectEnv returns inherited with BOTH project-scope variables removed, then sets ProjectDirEnv to projectDir when projectDir is non-empty. It is the one implementation of "hand a charly child process a project scope", shared by every module that spawns one.

Both names must be handled together, which is the whole reason this is a function rather than two constants each caller filters by hand: they are MUTUALLY EXCLUSIVE in the CLI, so a caller that strips ProjectDirEnv and leaves an inherited ProjectRepoEnv makes the child exit on "--repo and --dir are mutually exclusive" instead of running. Getting one right and the other wrong fails at runtime, not at compile time.

Stripping is the default because a child's project scope is almost never the parent's: the packaging child runs under the project owning the candy it packages, and the MCP server's forked charly must reach its own --repo default rather than inherit the caller's directory.

func ComputeCalVer added in v0.2026213.1726

func ComputeCalVer() string

ComputeCalVer returns the canonical build tag for the current UTC instant.

func ComputeCalVerAt added in v0.2026213.1726

func ComputeCalVerAt(t time.Time) string

ComputeCalVerAt formats t as the canonical CalVer: 4-digit year, 3-digit zero-padded day-of-year, 4-digit zero-padded HHMM. Every component is fixed-width, so a plain lexicographic sort of CalVer strings is chronological.

func ContainerName added in v0.2026213.1726

func ContainerName(boxName string) string

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

func ContainerNameInstance added in v0.2026213.1726

func ContainerNameInstance(boxName, instance string) string

ContainerNameInstance returns the container name with an optional instance suffix.

func CurrentUsername added in v0.2026210.1121

func CurrentUsername() string

CurrentUsername returns $USER or falls back to "charly" if neither $USER nor $LOGNAME is set.

func DecodePluginKindMap added in v0.2026210.808

func DecodePluginKindMap[T any](uf *UnifiedFile, kind string) map[string]*T

DecodePluginKindMap reconstructs the typed name-keyed map[string]*T for a plugin kind from uf.PluginKinds[kind] (each body the canonical T JSON the kind plugin's Invoke produced) via a PLAIN json.Unmarshal — no plugin OpResolve round-trip, for a kind whose body is already self-contained (Builder). Compare ResolvePluginKindViaPlugin above, its sibling for kinds that DO need a plugin-side resolve leg. A bad entry is skipped rather than poisoning the whole vocabulary. Returns nil when the kind has no entities.

func DedupeNonEmpty added in v0.2026218.138

func DedupeNonEmpty(in []string) []string

DedupeNonEmpty trims each entry and returns the input in order with blanks and repeats removed. Returns nil for an input that yields no entries (a nil-vs-empty distinction every caller relies on for its "no tokens declared" early return).

func DefaultDeployConfigPath added in v0.2026213.1726

func DefaultDeployConfigPath() (string, error)

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

func DeployKey added in v0.2026211.549

func DeployKey(boxName, instance string) string

DeployKey builds the charly.yml deployment map key from a box name and an optional instance: "selkies-desktop" (no instance) or "selkies-desktop/foo" (instance "foo"). The inverse is ParseDeployKey.

func DeriveDeploymentName added in v0.2026211.549

func DeriveDeploymentName(imageRef string) string

DeriveDeploymentName turns "quay.io/myorg/openclaw:v1" → "openclaw" and "registry.example.com/path/foo" → "foo" — the shared default-name derivation for a source-less `charly fleet from-box` deploy (both the pod path, candy/plugin-fleet/from_box_pod.go, and the kubernetes path, candy/plugin-fleet/deploy_from_box.go — R3, one function, two callers).

func DistroTagChain added in v0.2026210.2052

func DistroTagChain(distro, version string) []string

DistroTagChain builds the most-specific-first distro tag chain used by the cascade resolver: [<distro>:<version>, <distro>] when a canonical version is known (e.g. ["ubuntu:24.04", "ubuntu"]), or just [<distro>] for a rolling distro with no version (arch/cachyos).

func EncodeBox

func EncodeBox(b BoxConfig) json.RawMessage

EncodeBox marshals an authored BoxConfig into its opaque body.

func EncodeInlineCandy added in v0.2026210.808

func EncodeInlineCandy(il *InlineCandy) json.RawMessage

EncodeInlineCandy marshals a loader InlineCandy into its opaque body.

func ExpandPath added in v0.2026213.1726

func ExpandPath(path string, home string) string

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

Relocated from sdk/kit/env.go (#55 import-purity cone-render): a pure fabric path helper with no host dependency. kit.ExpandPath now aliases spec.ExpandPath (R3, one source), and spec.BuildServiceRenderContext consumes it in-package.

func ExternalBuilderPluginRef added in v0.2026218.138

func ExternalBuilderPluginRef(word string) (string, bool)

ExternalBuilderPluginRef returns the canonical @github ref to the candy serving an externalized builder word, and whether the word is a first-party externalized builder. It feeds the ONE on-demand connect path — ops.InvokeProviderOpts.ExtraRef, which the host resolves through connectPluginByWordRef's Pass-2 canonical-ref fetch — so a box/<distro> submodule build that triggers a builder by DETECTION but vendors the plugin candy nowhere still connects it. Living in spec (beside the word maps it reads) lets BOTH charly core and the plugin-side render reach it over a spec-only import surface.

func FinalizeCandyRefs

func FinalizeCandyRefs(m *CandyModel, v *CandyView, refs CandyRefs)

FinalizeCandyRefs projects the RICH pre-qualification refs (CandyRefs) down to their bare-string FINAL wire form — v.Require / v.IncludedCandy (CandyView) and m.BakePlugin (CandyModel) — run ONCE, after any remote-sibling qualification (loaderkit.QualifyRemoteSiblingDeps) has had the chance to set .Resolved. Mirrors the pre-move projectCandyView/projectCandyModel bare-ref projection (charly/resolved_project_host.go's bareRefs() calls, pre-W9), which ran AFTER qualifyRemoteSiblingDeps on the live *Candy. Lives in spec (not loaderkit, alongside its sibling CandyRefEntry/ToCandyRefEntries) — a pure, non-registry-coupled data-shape transform charly core must reach directly at the fix-point arbitration's final step (import-purity: core imports spec but never an sdk mechanism kit).

func FirstYAMLVersionLine added in v0.2026213.1726

func FirstYAMLVersionLine(data []byte) string

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

func FlattenedEnvMap added in v0.2026213.1726

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

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

func FleetDelArgv added in v0.2026219.1057

func FleetDelArgv(name string) []string

FleetDelArgv returns the argv (everything after the charly binary) for a non-interactive `charly fleet del <name>`: the verb, the name, and the ONE valid skip-confirmation flag. Every programmatic teardown builds its command through this single helper — in-process (proc.RunCharlySubcommand), out-of-process (exec.Command), and the systemd-run TTL timer — so the flag can never drift across call sites again (R3 hoist: this was byte-identically duplicated in charly core, candy/plugin-fleet, and candy/plugin-substrate).

The flag is `--assume-yes`, NOT `--yes`/`--force`: the command:fleet plugin's `charly fleet del` Kong grammar (candy/plugin-fleet) declares its AssumeYes field `name:"assume-yes"`, with `-y` as the short form. That tag used to read `long:"yes"` — go-flags syntax Kong ignores, so the flag was only ever --assume-yes by accident of Kong's field-name derivation; the whole tree was converted to `name:` so the declaration states what ships. A `--yes`/`--force` drift — neither of which Kong accepts — once aborted teardown at arg-parse and silently leaked the resource (see CHANGELOG/); the deploy-del-flag regression test guards this.

func FormatCacheMount added in v0.2026213.1726

func FormatCacheMount(dst, sharing string, uid, gid int) string

FormatCacheMount renders ONE cache mount as a Containerfile `--mount=type=cache,…` flag. uid>=0 → the owned form (uid/gid baked into the id namespace so different-uid builds don't collide on file ownership inside the cache volume); uid<0 → the shared form (sharing mode, defaulting to "locked" when empty — the BuildKit default for root-installed system caches). The id is derived from dst (and uid for owned), keeping the cache stable across layer-hash churn during iterative builds — the entire reason a derived id exists.

func FormatPhaseTemplate added in v0.2026213.1726

func FormatPhaseTemplate(f *Format, phase Phase, venue Venue) string

FormatPhaseTemplate looks up the template string for a (phase, venue) lookup. The phase: block is the single source of truth — the legacy top-level InstallTemplate field was removed (R5) and its content migrated into phase.install, so there is no fallback arm. A lookup returns "" when the requested cell is absent.

R3 (one canonical body, venue applied at render): the install cell is the venue-agnostic body written with `&& \` continuations (valid plain shell AND inside a Dockerfile RUN). The container venue wraps it with the BuildKit cacheMounts RUN prefix; the host venue returns it verbatim. Venue-specific OVERRIDES (host/container) take precedence when present, so a phase that genuinely differs by venue keeps its two cells without forcing a second canonical copy.

func FormatPortMapping added in v0.2026213.1726

func FormatPortMapping(p ParsedPortMapping) string

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

func GateEnabled added in v0.2026210.2308

func GateEnabled(g Gate, opts EmitOpts) bool

GateEnabled returns whether the given gate is permitted under opts. GateNone is always enabled; named gates require the corresponding opt-in flag.

func GitRemoteIdentity added in v0.2026213.1726

func GitRemoteIdentity(dir string) string

GitRemoteIdentity returns the normalized `host/owner/repo` identity of dir's git `origin` remote, or "" when dir is not a git repo / has no origin / git is unavailable. Cached process-wide per directory (the identity is stable for the process lifetime).

func InputStr added in v0.2026213.1726

func InputStr(c *Op, key string) string

InputStr reads a string field from the step's desugared plugin input map.

func IsExternalStepKind added in v0.2026210.2308

func IsExternalStepKind(k StepKind) bool

IsExternalStepKind reports whether k is an external (plugin-contributed) step kind.

func IsRemoteCandyRefString

func IsRemoteCandyRefString(ref string) bool

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

func IsRemoteImageRef

func IsRemoteImageRef(ref string) bool

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

func IsValidCheckLevel added in v0.2026213.1726

func IsValidCheckLevel(level string) bool

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

func LeafName

func LeafName(ref string) string

LeafName strips every namespace prefix from a (possibly qualified) ref, returning the final member name — e.g. "charly.arch-builder" -> "arch-builder", "a.b.c" -> "c", bare "fedora" -> "fedora".

func MappingRoot

func MappingRoot(n *yaml.Node) *yaml.Node

MappingRoot unwraps a YAML document node to its top-level mapping node (or nil). Shared by charly core and the out-of-module candy/plugin-migrate engine (R3).

func MergePluginKindsMap added in v0.2026213.1726

func MergePluginKindsMap(dst *map[string]map[string]json.RawMessage, src map[string]map[string]json.RawMessage)

MergePluginKindsMap merges src's plugin-kind entities INTO *dst under project-wins semantics: an entity already present in dst (by kind, then name) is never overwritten. A pure map/json.RawMessage merge over the plugin-kind carrier shape, relocated to the dedicated spec module (#55 2b) so the binary-embedded-defaults merge (charly/embed_defaults.go) reaches it without importing loaderkit; loaderkit re-exports it as a forwarder for its own merge path.

func NormalizeGitRemoteURL added in v0.2026213.1726

func NormalizeGitRemoteURL(raw string) string

NormalizeGitRemoteURL strips the scheme / `git@` / `.git` decorations from a git remote URL, leaving `host/owner/repo`. scp-style (`git@host:owner/repo`) and scheme URLs (`https://`, `ssh://`, `git://`, with optional `user@`) are both handled. A value already in `host/owner/repo` (or bare `owner/repo`) form passes through unchanged.

func NormalizePCIVendor

func NormalizePCIVendor(s string) string

NormalizePCIVendor lowercases a PCI vendor id and ensures the "0x" prefix, so "10DE" / "0X10de" / "0x10de" all compare equal to the sysfs form ("0x10de").

func NormalizeRepoIdentity added in v0.2026213.1726

func NormalizeRepoIdentity(s string) string

NormalizeRepoIdentity normalizes an explicit `repo:` value (which may be a full git URL, an scp-style ref, or a bare `owner/repo`) to the `host/owner/repo` form ParseRemoteRef produces — so an explicit declaration and a remote `@`-ref compare equal. Reuses NormalizeRepoSpec's bare-`owner/repo` → github.com rule.

func NormalizeRepoSpec added in v0.2026213.1726

func NormalizeRepoSpec(spc string) (repoPath, version string)

NormalizeRepoSpec turns a user-supplied --repo spec into a (repoPath, version) pair suitable for a repo-cache download. Spec formats:

"default"               → (DefaultProjectRepo, "")
"owner/repo"            → ("github.com/owner/repo", "")
"owner/repo@ref"        → ("github.com/owner/repo", "ref")
"host/owner/repo[@ref]" → used literally

An empty version means "resolve to default branch at lookup time".

func PackageNames

func PackageNames(items []PackageItem) []string

PackageNames returns just the names from a PackageItem list, in order. Convenience for places that only need the install-target list.

func ParseDeployKey added in v0.2026211.549

func ParseDeployKey(key string) (boxName, instance string)

ParseDeployKey splits a charly.yml map key back into image name and instance. "selkies-desktop" → ("selkies-desktop", "") "selkies-desktop/foo" → ("selkies-desktop", "foo")

func PathIsSystemScoped added in v0.2026210.2308

func PathIsSystemScoped(p string) bool

PathIsSystemScoped returns true for paths under /etc, /usr, /var, /opt, /root, /boot and similar system locations. Used to classify FileStep scope when the compiler hasn't explicitly tagged it.

func PodName added in v0.2026213.1726

func PodName(boxName string) string

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

func PodNameInstance added in v0.2026213.1726

func PodNameInstance(boxName, instance string) string

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

func PodQuadletFilename added in v0.2026213.1726

func PodQuadletFilename(boxName string) string

PodQuadletFilename returns the quadlet filename for a pod.

func PodQuadletFilenameInstance added in v0.2026213.1726

func PodQuadletFilenameInstance(boxName, instance string) string

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

func QuadletDir added in v0.2026213.1726

func QuadletDir() (string, error)

QuadletDir returns the user-level quadlet directory.

func QuadletExists added in v0.2026213.1726

func QuadletExists(boxName string) (bool, error)

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

func QuadletExistsInstance added in v0.2026213.1726

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

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

func QuadletFilename added in v0.2026213.1726

func QuadletFilename(boxName string) string

QuadletFilename returns the quadlet filename for an image.

func QuadletFilenameInstance added in v0.2026213.1726

func QuadletFilenameInstance(boxName, instance string) string

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

func RenderCacheMounts added in v0.2026213.1726

func RenderCacheMounts(mounts []CacheMount, uid, gid int, sep string, trailing bool) string

RenderCacheMounts joins a slice of spec.CacheMount into one Containerfile flag string. uid<0 → shared form (sharing-locked); uid>=0 → owned form. `trailing` appends the separator after the last entry — needed by cacheMountsOwned which feeds directly into a multi-line RUN body. Single source of truth for the slice-rendering pattern that previously lived inline at four call sites (two template helpers + the former generate.go + tasks.go cmd-emitter); every multi-mount site now flows through here, every single-mount site flows through FormatCacheMount (via buildkit's CacheMount.String()) directly.

func RenderCacheMountsAuto added in v0.2026213.1726

func RenderCacheMountsAuto(mounts []CacheMount, uid, gid int, sep string, trailing bool) string

RenderCacheMountsAuto renders a MIXED list where each entry is owned (uid/gid) or shared per its own `owned:` flag — letting one builder declare both root system caches (pacman → shared/locked) and user build caches (makepkg SRCDEST, yay AUR clones → uid/gid-owned) in a single cache_mount list. uid/gid apply only to the entries flagged owned; the rest render in the shared form.

func RepoIdentity added in v0.2026213.1726

func RepoIdentity(ref, baseDir string) string

RepoIdentity returns the canonical repo identity of an import ref, or "" when it can't be determined (in which case the loader degrades to version-keyed behavior). A remote `@host/org/repo[/sub]:ver` ref yields `host/org/repo` directly (no fetch, no git); a local path yields the git `origin` identity of the target directory.

func ResolveBoxName added in v0.2026213.1726

func ResolveBoxName(box string) string

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

func ResolveCheckLevel added in v0.2026213.1726

func ResolveCheckLevel(level string) string

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

func ResolveHome added in v0.2026210.2308

func ResolveHome(p *InstallPlan, home string)

ResolveHome substitutes the deferred HomeToken with a concrete home in every home-bearing step field, in place. Each EmitTarget calls this once at emit time with the home of its real destination: img.Home for the OCI/pod-overlay build, the host home for the external local deploy, the GUEST home (SSH executor ResolveHome) for the external vm deploy. Idempotent — fields without the token are left untouched, so a second call is a no-op.

Covered fields: ShellHookStep env values + PathAdd, ShellSnippetStep Snippet + Destination + PathAppend, FileStep.Dest. OpStep cmd/content bodies are intentionally NOT touched — `~`/`$HOME` there shell-expand at runtime on the destination as the deploy user, which is already correct on every venue. BuilderStep is also untouched — its home is resolved separately by renderBuilderScript against the builder/guest home (see execBuilder).

A free function (not a method on InstallPlan) because it type-switches the concrete step vocabulary.

func ResolvePluginKindViaPlugin added in v0.2026210.808

func ResolvePluginKindViaPlugin[T any](uf *UnifiedFile, kind string, resolve func(json.RawMessage) (*T, error)) map[string]*T

ResolvePluginKindViaPlugin projects uf.PluginKinds[kind] (opaque bodies) into *T value envelopes via resolve, a per-body plugin OpResolve leg — the ONE shared loop every "resolve a plugin-kind catalog through its OpResolve leg" accessor uses (charly-core's Distros/Builders/ resolveResources/resolveAndroids/resolveInits each supply their own registry-coupled resolve callback; this loop itself touches no registry). A bad entry is skipped rather than poisoning the whole vocabulary.

func RootRepoIdentity added in v0.2026213.1726

func RootRepoIdentity(dir string) string

RootRepoIdentity determines the local root project's own repo identity for cycle-break registration. An explicit `repo:` field in charly.yml is authoritative; otherwise it falls back to the `git remote origin` identity of the working tree. Returns "" when neither is available (the loader then behaves exactly as before — version-keyed, no self-identity short-circuit).

func ScalarNode

func ScalarNode(v string) *yaml.Node

ScalarNode builds a string scalar YAML node.

func ServiceName added in v0.2026213.1726

func ServiceName(boxName string) string

ServiceName returns the systemd service name for an image.

func ServiceNameInstance added in v0.2026213.1726

func ServiceNameInstance(boxName, instance string) string

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

func SidecarContainerName added in v0.2026213.1726

func SidecarContainerName(boxName, sidecarName string) string

SidecarContainerName returns the container name for a named sidecar.

func SidecarContainerNameInstance added in v0.2026213.1726

func SidecarContainerNameInstance(boxName, instance, sidecarName string) string

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

func SidecarQuadletFilename added in v0.2026213.1726

func SidecarQuadletFilename(boxName, sidecarName string) string

SidecarQuadletFilename returns the quadlet filename for a sidecar container.

func SidecarQuadletFilenameInstance added in v0.2026213.1726

func SidecarQuadletFilenameInstance(boxName, instance, sidecarName string) string

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

func SortedDeployKeys added in v0.2026213.1726

func SortedDeployKeys(m map[string]FleetNode) []string

SortedDeployKeys returns the deploy-map keys in deterministic (sorted) order.

func SortedMemberKeys added in v0.2026213.1726

func SortedMemberKeys(members map[string]*FleetNode) []string

SortedMemberKeys returns the member keys of a node in deterministic order.

func SplitNamespaceRef

func SplitNamespaceRef(ref string) (ns, rest string, ok bool)

SplitNamespaceRef splits a qualified ref on its FIRST `.` into (namespace, remainder). A bare ref (no dot, or a leading/trailing dot) returns ok=false. The remainder may itself be qualified (`a.b.c` → "a", "b.c").

func SplitRepoAndSubPath

func SplitRepoAndSubPath(ref string) (repoPath, subPath, name string)

SplitRepoAndSubPath splits a ref into repo path (host/org/repo), sub-path, and name. e.g. "github.com/org/repo/candy/name" -> ("github.com/org/repo", "candy/name", "name") For short refs like "pixi", returns ("", "", "pixi").

func SplitVmAddress added in v0.2026213.1726

func SplitVmAddress(name string) (plain string, isVm bool)

SplitVmAddress detects the "vm:"-prefixed CLI ADDRESSING form (`charly fleet add/del vm:<name>` / `vm:<parent.child>`) and returns the address with that prefix stripped, plus whether it was present. "vm:" here is an ADDRESSING HINT — "resolve this via the vm substrate" — NEVER an identity itself; a caller that needs the plain (tree-lookup / ledger-identity) form strips it via this helper, one which needs the sanitized dc.Fleet key form still applies "vm:"+VmDomainIdentity(...) separately (a DIFFERENT canonical form).

NOT the same job as VmNameFromDeployName (which extracts the VM ENTITY and errors when the prefix is ABSENT — a different, already-established, unchanged contract) or VmDomainIdentity (which sanitizes dots/slashes for a domain-identity STRING, unconditionally, prefix or not). Relocated from sdk/vmshared (#55 vmshared Bucket B); vmshared keeps a re-export forwarder.

func StampDescent added in v0.2026213.1726

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

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

func StatusFromState

func StatusFromState(state string) string

StatusFromState normalises an engine/container runtime state vocabulary to charly's DeploymentStatus.Status vocabulary. Shared by the substrate plugins' status collectors (the pod live row builder in candy/plugin-substrate) AND the host-side vm collector (charly/status_collect_vm.go, deploy-cone-coupled → stays core until K5) — single-sourced here (R3) so the live/enrich split (P14a) and the K5 vm-collector move both read ONE mapper. Pure over its input.

func StripCandyRefVersion

func StripCandyRefVersion(ref string) (string, string)

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

func StripPortSuffix added in v0.2026213.1726

func StripPortSuffix(s string) (string, string)

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

func StripURLScheme added in v0.2026213.1726

func StripURLScheme(ref string) string

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

func SystemdUserDir added in v0.2026213.1726

func SystemdUserDir() (string, error)

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

func TrimPreview added in v0.2026213.1726

func TrimPreview(s string) string

TrimPreview truncates s to a 200-char preview (trailing "…") for compact check-output display. Homed in the fabric slice github.com/opencharly/spec/spec (the same fabric-primitive class as shellquote.ShellQuote) so both the executor slice (spec/exec) and kit reach the ONE copy — kit re-exports it (kit.TrimPreview) so its existing callers compile unchanged (R3, single source).

func ValidateDeployRequiresBox

func ValidateDeployRequiresBox(deploy map[string]FleetNode) error

ValidateDeployRequiresBox enforces the 2026-05-12 schema rule: every `target: pod` deploy entry MUST declare its `box:` field. Pre-cutover the check runner silently fell back to inspecting the running container's image ref via `containerImageRef`, which read stale OCI labels off volume-pinned containers and dropped any probes added after the seed image. The hard-required field forces operator intent to be explicit; the check runner now resolves the ref ONLY from this field.

Scope: target: pod (or empty — pod is the default). target: vm uses `vm:`, target: local is candy-driven, target: kubernetes CLUSTER definitions live in the `kubernetes:` section (not deploy:).

Remediation: `charly migrate` (idempotent) walks every affected deploy and injects the field, inferring the value from the deploy key (`<base>` for `<base>/<instance>` keys; the key itself otherwise).

func ValidateDeploymentChildren

func ValidateDeploymentChildren(path string, node *FleetNode) error

ValidateDeploymentChildren recurses ValidateDeploymentName over node's nested-deployment children.

func ValidateDeploymentName

func ValidateDeploymentName(name, parentPath string) error

ValidateDeploymentName rejects a deploy-tree key containing ".", reserved for dotted-path CLI addressing (`charly fleet add a.b.c`).

func ValidateDeploymentTree

func ValidateDeploymentTree(deploy map[string]FleetNode) error

ValidateDeploymentTree enforces structural invariants on the deployments tree that can't be expressed in the YAML struct tags:

  • Map keys at every level MUST NOT contain "." (dots are reserved for dotted-path CLI addressing like `charly fleet add a.b.c`).
  • Every explicit pod deploy must declare `box:` (ValidateDeployRequiresBox).

Errors include the offending path so the user sees exactly which entry needs to be fixed.

func VenueIsAgentProvisioned added in v0.2026213.1726

func VenueIsAgentProvisioned(uf *UnifiedFile, venue string) bool

VenueIsAgentProvisioned reports whether the deploy node named venue (a child or member anywhere in the fleet tree) is flagged AgentProvisioned — the ONE genuinely fleet-tree-coupled predicate the check-run preflight needs to skip an agent-provisioned image's local-storage ensure.

func VmDomainIdentity

func VmDomainIdentity(deployName string) string

VmDomainIdentity normalizes a deploy/fleet name into the per-deploy VM DOMAIN IDENTITY — the token that keys the libvirt domain (charly-<identity>), the per-domain state dir, the managed ssh-config alias, and the ssh-port ledger entry (vm:<identity>). It is DISTINCT from the kind:vm ENTITY (the disk/spec source, resolved via the deploy's `from:` cross-ref): several distinct beds may share one entity, so keying the domain by the ENTITY collided them on one libvirt domain + one disk + one host SSH port. Keying by the DEPLOY NAME instead makes distinct beds collision-free by construction (each gets its own domain, per-deploy disk overlay, and auto-allocated port).

The normalization strips a leading "vm:" prefix and flattens the instance ("/") and nested-path (".") separators to "-", so a bare bed name maps to itself (check-builder-vm → check-builder-vm), a fleet ref maps to its VM token (vm:arch → arch), and a direct `charly vm create <entity>` (whose domain identity IS the entity) is unchanged. Both the host preresolver and candy/plugin-deploy-vm call this on the SAME deploy name, so the domain they derive always agrees.

Home note (FLOOR-legal leaf): lives in package spec — the always-floor-legal wire/vocabulary leaf — so BOTH charly core and a plugin (candy/plugin-check's bed_session.go, #55 W3 B2-full) derive the domain identity without a vmshared import. stdlib-only (strings).

func VmNameFromDeployName added in v0.2026213.1726

func VmNameFromDeployName(deployName string) (string, error)

VmNameFromDeployName extracts the VM entity name from a deploy-key in the legacy "vm:<name>[/<instance>]" form. Callers that hold a schema-v4 deploy key (whose entity comes from the node's `vm:` field) resolve the entity a different way (the node's own From field); this helper handles the prefixed form (legacy refs + the "vm:<entity>" key the del path builds for ledger/teardown keying). The `instance` suffix is preserved for future per-instance addressing but currently unused. Relocated from sdk/vmshared (#55 vmshared Bucket B) so kernel-floor charly files address VM deploys without a vmshared import; vmshared keeps a re-export forwarder.

func VmSshAlias added in v0.2026213.1726

func VmSshAlias(vmName string) string

VmSshAlias returns the canonical managed-ssh-config Host alias for a VM deployment name ("charly-" namespaced) — the token `charly vm create` / the vm deploy lifecycle writes the Host stanza under (~/.config/charly/ssh_config) and an SSHExecutor built with just `Host: VmSshAlias(id)` lets ssh(1) supply User/Port/IdentityFile. Pure naming leaf, a peer of VmDomainIdentity: lives in package spec — the always-floor-legal wire/vocabulary leaf — so the spec/exec executor-chain constructors (RootExecutorForDeployNode/ResolveDeployChain/ VmChildExecutor) derive the alias without an sdk/kit import (#55 K4). sdk/kit re-exports it for the host-coupled ssh-config-fragment writers (renderStanza/VmSshStanza) that STAY in kit.

func WrapContainerCommand added in v0.2026213.1726

func WrapContainerCommand(script string) string

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

Types

type AUR

type AUR struct {
	Package []PackageItem `yaml:"package,omitempty" json:"package,omitempty"`

	Options []string `yaml:"option,omitempty" json:"option,omitempty"`

	Replaces []string `yaml:"replace,omitempty" json:"replace,omitempty"`
}

type AdbEndpoint

type AdbEndpoint struct {
	Host string `yaml:"host,omitempty" json:"host"`
}

adb endpoint host is "host:port" but MAY carry a ${HOST_PORT:N} var-ref (resolved at deploy time), so only non-emptiness is enforced.

type Agent

type Agent struct {
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Command []string `yaml:"command,omitempty" json:"command"`

	PromptVia string `yaml:"prompt_via,omitempty" json:"prompt_via"`

	VersionCommand []string `yaml:"version_command,omitempty" json:"version_command,omitempty"`

	Timeout Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`

	Credential []CredentialMount `yaml:"credential,omitempty" json:"credential,omitempty"`

	ProgressCheckInterval Duration `yaml:"progress_check_interval,omitempty" json:"progress_check_interval,omitempty"`

	ProgressNoImprovementTimeout Duration `yaml:"progress_no_improvement_timeout,omitempty" json:"progress_no_improvement_timeout,omitempty"`

	OutputFormat string `yaml:"output_format,omitempty" json:"output_format"`
}

type AgentAbortControl

type AgentAbortControl struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id"`

	RequestID UUIDv7 `yaml:"request_id,omitempty" json:"request_id"`

	RequestedAt string `yaml:"requested_at,omitempty" json:"requested_at"`
}

type AgentDelegationEdge

type AgentDelegationEdge struct {
	From string `yaml:"from,omitempty" json:"from"`

	To string `yaml:"to,omitempty" json:"to"`

	Allow []string `yaml:"allow,omitempty" json:"allow,omitempty"`
}

type AgentEvent

type AgentEvent struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id"`

	Sequence int64 `yaml:"sequence,omitempty" json:"sequence"`

	Type string `yaml:"type,omitempty" json:"type"`

	Time string `yaml:"time,omitempty" json:"time"`

	Payload map[string]any `yaml:"payload,omitempty" json:"payload,omitempty"`
}

type AgentExecSpec

type AgentExecSpec struct {
	Command []string `yaml:"command,omitempty" json:"command,omitempty"`

	PromptVia string `yaml:"prompt_via,omitempty" json:"prompt_via,omitempty"`

	VersionCommand []string `yaml:"version_command,omitempty" json:"version_command,omitempty"`

	Timeout Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`

	Credential []CredentialMount `yaml:"credential,omitempty" json:"credential,omitempty"`

	ProgressCheckInterval Duration `yaml:"progress_check_interval,omitempty" json:"progress_check_interval,omitempty"`

	ProgressNoImprovementTimeout Duration `yaml:"progress_no_improvement_timeout,omitempty" json:"progress_no_improvement_timeout,omitempty"`

	OutputFormat string `yaml:"output_format,omitempty" json:"output_format,omitempty"`
}

#AgentExecSpec is a fully-resolved long-running-CLI invocation descriptor — the resolve-to-envelope form of an agent, with Go-level defaults applied by the plugin (Timeout, PromptVia). Kind-agnostic: it describes HOW to launch a CLI, capture its version, and poll it in a plateau-bounded loop — not "agent-kind" knowledge.

type AgentFederationRecord

type AgentFederationRecord struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	Node string `yaml:"node,omitempty" json:"node"`

	Owner string `yaml:"owner,omitempty" json:"owner"`

	SessionID UUIDv7 `yaml:"session_id,omitempty" json:"session_id,omitempty"`

	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id,omitempty"`

	State string `yaml:"state,omitempty" json:"state"`

	UpdatedAt string `yaml:"updated_at,omitempty" json:"updated_at"`

	Metadata StrMap `yaml:"metadata,omitempty" json:"metadata,omitempty"`
}

type AgentResolveInput

type AgentResolveInput struct {
	Agents map[string]RawBody `yaml:"agents,omitempty" json:"agents,omitempty"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`
}

#AgentResolveInput is candy/plugin-agent's OpResolve input: the opaque agent bodies (the name-keyed AI-CLI catalog) + the selected agent name ("" picks the sole entry, or errors when several are configured).

type AgentResolveReply

type AgentResolveReply struct {
	Spec *AgentExecSpec `yaml:"spec,omitempty" json:"spec,omitempty"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`
}

#AgentResolveReply is the OpResolve reply: the resolved exec spec + the chosen agent's catalog name.

type AgentRunRequest

type AgentRunRequest struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	SessionID UUIDv7 `yaml:"session_id,omitempty" json:"session_id"`

	RequestID UUIDv7 `yaml:"request_id,omitempty" json:"request_id"`

	IdempotencyKey string `yaml:"idempotency_key,omitempty" json:"idempotency_key"`

	Prompt string `yaml:"prompt,omitempty" json:"prompt,omitempty"`

	Params map[string]any `yaml:"params,omitempty" json:"params,omitempty"`

	Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"`
}

type AgentRuntimeCapability

type AgentRuntimeCapability struct {
	Provider string `yaml:"provider,omitempty" json:"provider"`

	ProtocolVersion int `yaml:"protocol_version,omitempty" json:"protocol_version"`

	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"`

	InteractionModes []string `yaml:"interaction_modes,omitempty" json:"interaction_modes,omitempty"`

	Persistence string `yaml:"persistence,omitempty" json:"persistence,omitempty"`

	SemanticCompletion bool `yaml:"semantic_completion,omitempty" json:"semantic_completion,omitempty"`
}

type AgentSession

type AgentSession struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	Runtime string `yaml:"runtime,omitempty" json:"runtime"`

	Target TargetSpec `yaml:"target,omitempty" json:"target"`

	State string `yaml:"state,omitempty" json:"state"`

	CreatedAt string `yaml:"created_at,omitempty" json:"created_at"`

	UpdatedAt string `yaml:"updated_at,omitempty" json:"updated_at"`

	StorageRef string `yaml:"storage_ref,omitempty" json:"storage_ref,omitempty"`

	Metadata StrMap `yaml:"metadata,omitempty" json:"metadata,omitempty"`

	TerminalProfile *TerminalProfile `yaml:"terminal_profile,omitempty" json:"terminal_profile,omitempty"`
}

type AgentSessionBinding

type AgentSessionBinding struct {
	StorageRef string `yaml:"storage_ref,omitempty" json:"storage_ref"`
}

Provider-reported durable session correlation. Runtime-specific session state remains on the target; this is the only binding replicated to the controller.

type AgentTeam

type AgentTeam struct {
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Agents []AgentTeamMember `yaml:"agents,omitempty" json:"agents"`

	Edges []AgentDelegationEdge `yaml:"edges,omitempty" json:"edges,omitempty"`

	Coordinator string `yaml:"coordinator,omitempty" json:"coordinator,omitempty"`

	Concurrency int64 `yaml:"concurrency,omitempty" json:"concurrency,omitempty"`

	EvidencePolicy string `yaml:"evidence_policy,omitempty" json:"evidence_policy,omitempty"`
}

type AgentTeamMember

type AgentTeamMember struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Runtime string `yaml:"runtime,omitempty" json:"runtime"`

	Role string `yaml:"role,omitempty" json:"role,omitempty"`

	Target TargetSpec `yaml:"target,omitempty" json:"target,omitempty"`

	TerminalProfile *TerminalProfile `yaml:"terminal_profile,omitempty" json:"terminal_profile,omitempty"`
}

type AgentTeamRecord

type AgentTeamRecord struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	Team AgentTeam `yaml:"team,omitempty" json:"team"`

	Sessions map[string]UUIDv7 `yaml:"sessions,omitempty" json:"sessions"`

	CreatedAt string `yaml:"created_at,omitempty" json:"created_at"`
}

Durable control-plane projection of a declarative team. The authored graph remains #AgentTeam; this record binds each member name to the ephemeral session created for one dispatch so later delegation can re-check authority.

type AggregatedCandyCaps added in v0.2026210.1533

type AggregatedCandyCaps struct {
	PreserveUser bool `yaml:"preserve_user,omitempty" json:"preserve_user,omitempty"`

	NeedsRootAfterInit bool `yaml:"needs_root_after_init,omitempty" json:"needs_root_after_init,omitempty"`

	InitSystemHint string `yaml:"init_system_hint,omitempty" json:"init_system_hint,omitempty"`

	DataOnly bool `yaml:"data_only,omitempty" json:"data_only,omitempty"`

	OCILabels map[string]string `yaml:"oci_labels,omitempty" json:"oci_labels,omitempty"`

	// provided is the set of capability names declared by some candy in the composition.
	// Used by CheckRequiredCapabilities to validate `requires_capabilities:` cross-candy
	// requirements.
	Provided map[string]bool `yaml:"provided,omitempty" json:"provided,omitempty"`
}

#AggregatedCandyCaps — the output of walking all candies in resolution order. Populated onto #ResolvedBox and consumed wherever code reads BoxConfig.Bootc/BoxConfig.DataImage/the init-system bootc parameter.

func AggregateCandyCapabilities added in v0.2026213.1726

func AggregateCandyCapabilities(layers map[string]CandyReader, order []string) (*AggregatedCandyCaps, error)

AggregateCandyCapabilities walks `order` (candy names in topological resolution order) and merges each candy's `capabilities:` contribution. Returns an error if two candies declare conflicting values for the same OCI label key — the conflict surfaces the bug rather than silently picking a winner.

type AggregatedCandyCapsView

type AggregatedCandyCapsView struct {
	PreserveUser bool `yaml:"preserve_user,omitempty" json:"preserve_user,omitempty"`

	NeedsRootAfterInit bool `yaml:"needs_root_after_init,omitempty" json:"needs_root_after_init,omitempty"`

	OCILabels map[string]string `yaml:"oci_labels,omitempty" json:"oci_labels,omitempty"`
}

#AggregatedCandyCapsView — the box-level aggregated candy capability surface the build RENDER reads (a wire projection of buildkit.AggregatedCandyCaps): the arbitrary candy-declared oci_labels + the two booleans that gate the render (preserve_user → the final-USER-reset skip + the bootc round-trip label; needs_root_after_init → the post-candy root reset). Carried so the plugin-side render (NewSpecResolvedBox) reattaches img.CandyCaps WITHOUT re-aggregating over the live *Candy graph.

type AliasConfig

type AliasConfig = BoxAlias

--- box / deploy sub-types ---

type AliasYAML

type AliasYAML = CandyAlias

--- candy sub-types ---

type AlpineBootstrap

type AlpineBootstrap struct {
	MirrorURL string `yaml:"mirror_url,omitempty" json:"mirror_url,omitempty"`
}

type Android

type Android struct {
	Serial string `yaml:"serial,omitempty" json:"serial,omitempty"`

	Device string `yaml:"device,omitempty" json:"device,omitempty"`

	ApiLevel int `yaml:"api_level,omitempty" json:"api_level,omitempty"`

	GoogleAccount *GoogleAccount `yaml:"google_account,omitempty" json:"google_account,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	// Device source — exactly one of box/adb (enforced in Go, see header).
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Adb *AdbEndpoint `yaml:"adb,omitempty" json:"adb,omitempty"`
}

CUE schema for the `android` kind. #Android validates ONE kind:android entity (a device). A device is EITHER an in-pod emulator hosted by a kind:box (box) XOR a remote/physical adb endpoint (adb) — never both, never neither. Shared #Step from _common.cue.

CLOSED: the base struct (no trailing `...`) models every authored key, so an unknown key is still a typo. The box⊻adb exactly-one mutual-exclusion is enforced in GO (loaderkit.ValidateAndroidDevices, the load-time sibling of validateCheckBeds) — NOT by a trailing `& ({box:_} | {adb:_})` disjunction: `cue exp gengotypes` collapses an entity-level top-level disjunction to an EMPTY `struct{}`, which would make spec.Android useless as a drop-in for AndroidSpec. The plain closed struct below generates the full field set, and the Go rule restores the exactly-one-of-box/adb XOR.

func (*Android) EffectiveSerial

func (a *Android) EffectiveSerial() string

EffectiveSerial returns the device serial, defaulting to the emulator serial. Shares the serial core with the *ResolvedAndroid twin (spec/substrate_template_methods.go).

func (*Android) IsEndpoint

func (a *Android) IsEndpoint() bool

IsEndpoint reports whether the device targets a remote adb endpoint. Shares the endpoint core with the *ResolvedAndroid twin (spec/substrate_template_methods.go).

type AndroidDeployVenue

type AndroidDeployVenue struct {
	AdbAddr string `yaml:"adb_addr,omitempty" json:"adb_addr"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	Container string `yaml:"container,omitempty" json:"container,omitempty"`

	Serial string `yaml:"serial,omitempty" json:"serial,omitempty"`

	GoogleEmail string `yaml:"google_email,omitempty" json:"google_email,omitempty"`

	GoogleToken string `yaml:"google_token,omitempty" json:"google_token,omitempty"`

	Installs []ApkPackageSpec `yaml:"installs,omitempty" json:"installs,omitempty"`

	// boot_timeout / install_deadline / install_interval are the readiness +
	// install-retry windows the host ships (no magic numbers in the plugin).
	BootTimeout string `yaml:"boot_timeout,omitempty" json:"boot_timeout,omitempty"`

	InstallDeadline string `yaml:"install_deadline,omitempty" json:"install_deadline,omitempty"`

	InstallInterval string `yaml:"install_interval,omitempty" json:"install_interval,omitempty"`
}

#AndroidDeployVenue is the preresolved deploy:android substrate payload the host's android deploy preresolver produces (in DeployVenue.Substrate) and the candy/plugin-adb deploy:android provider decodes.

type AndroidResolveInput

type AndroidResolveInput struct {
	Android RawBody `yaml:"android,omitempty" json:"android"`
}

type AndroidResolveReply

type AndroidResolveReply struct {
	Resolved *ResolvedAndroid `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

type AndroidSpec

type AndroidSpec = Android

--- top-level entity types ---

type ApkInstallStep added in v0.2026210.2308

type ApkInstallStep struct {
	Packages  []ApkPackageSpec
	CandyName string
	CandyDir  string // candy source dir — anchors relative committed-APK paths
}

ApkInstallStep installs Android apps onto a `kind: android` device. It is the IR form of a candy's `apk:` package section — the apk "package format" (parallel to SystemPackagesStep for rpm/deb/pac, and BuilderStep for aur).

Unlike every other step, an apk install lands on a RUNNING Android device, not the build/host filesystem. `target: android` is an EXTERNAL deploy substrate (F1): NO in-proc EmitTarget executes this step — the host-side android deploy preresolver (collectAndroidInstalls, android_deploy_preresolve.go) READS it to collect the apk install specs and ships them to the deploy:android plugin (candy/plugin-adb: apkeep + adb), which drives the device. Every EmitTarget SKIPS it — OCITarget emits nothing (no device at image-build time), and Local/Vm/Pod targets record a skip (a host/VM/pod is not an Android device). This is the same "wrong venue → skip" shape `aur:` uses off-Arch, expressed as a clean recorded skip rather than an error (an image legitimately builds without installing apps).

func (*ApkInstallStep) Kind added in v0.2026210.2308

func (s *ApkInstallStep) Kind() StepKind

func (*ApkInstallStep) RequiresGate added in v0.2026210.2308

func (s *ApkInstallStep) RequiresGate() Gate

func (*ApkInstallStep) Reverse added in v0.2026210.2308

func (s *ApkInstallStep) Reverse() []ReverseOp

Reverse returns no STATIC ledger ops — Android teardown ops are DYNAMIC, recorded from the deploy:android plugin's OpExecute reply (the uninstall reverse ops) at deploy time, replayed at `charly fleet del` like any external deploy substrate.

func (*ApkInstallStep) Scope added in v0.2026210.2308

func (s *ApkInstallStep) Scope() Scope

Scope is system — installing an app mutates device-global package state.

func (*ApkInstallStep) Venue added in v0.2026210.2308

func (s *ApkInstallStep) Venue() Venue

Venue is host-native — the android deploy preresolver reads this step from the host (the install itself runs in the deploy:android plugin over the wire).

type ApkPackageSpec

type ApkPackageSpec struct {
	Package    string `yaml:"package,omitempty" json:"package,omitempty"`
	Apk        string `yaml:"apk,omitempty" json:"apk,omitempty"`
	Source     string `yaml:"source,omitempty" json:"source,omitempty"`
	Arch       string `yaml:"arch,omitempty" json:"arch,omitempty"`
	AppVersion string `yaml:"app_version,omitempty" json:"app_version,omitempty"`
}

--------------------------------------------------------------------------- ApkPackageSpec — #CandyApk (android_spec.go). package XOR apk. ---------------------------------------------------------------------------

func (ApkPackageSpec) EffectiveArch

func (s ApkPackageSpec) EffectiveArch() string

EffectiveArch returns the APK ABI, defaulting to x86_64.

func (ApkPackageSpec) EffectiveSource

func (s ApkPackageSpec) EffectiveSource() string

EffectiveSource returns the APK source store, defaulting to apk-pure.

type ArbiterInvokeInput

type ArbiterInvokeInput struct {
	Action string `yaml:"action,omitempty" json:"action"`

	Claimant string `yaml:"claimant,omitempty" json:"claimant,omitempty"`

	Tokens []string `yaml:"tokens,omitempty" json:"tokens,omitempty"`

	ClaimAddr HolderAddr `yaml:"claim_addr,omitempty" json:"claim_addr"`

	Transient bool `yaml:"transient,omitempty" json:"transient,omitempty"`

	Success bool `yaml:"success,omitempty" json:"success,omitempty"`

	Token string `yaml:"token,omitempty" json:"token,omitempty"`

	// GPU-implied-consumer projection (acquire-shared only, K-wave W3a A2): the claimant node's
	// GPU-relevant traits, so verb:arbiter can union its OWN implied-consumer token onto tokens
	// before applying its early-return-on-empty acquire policy — arbiter policy belongs in the
	// arbiter, not the in-core proxy (the former charly/gpu_imply.go, now
	// candy/plugin-preempt/gpu_imply.go). is_group/is_pod_member are pre-derived core-side (the
	// in-core nodeTraits/isPodMember trait resolution — not duplicated plugin-side);
	// security_devices is the claimant's raw security.devices list.
	IsGroup bool `yaml:"is_group,omitempty" json:"is_group,omitempty"`

	IsPodMember bool `yaml:"is_pod_member,omitempty" json:"is_pod_member,omitempty"`

	SecurityDevices []string `yaml:"security_devices,omitempty" json:"security_devices,omitempty"`
}

#ArbiterInvokeInput is the action-multiplexed input the in-core proxy ships to verb:arbiter. Each action populates only the field(s) it needs (the OTHERS stay Go zero-value); claim_addr is REQUIRED rather than optional because the original hand-written type's `omitempty` on a value-typed (non-pointer) HolderAddr field was already a no-op in encoding/json — Go's omitempty never elides a struct value — so marking it required here changes nothing about the actual wire bytes, only how honestly the schema describes them.

type ArbiterInvokeReply

type ArbiterInvokeReply struct {
	Active bool `yaml:"active,omitempty" json:"active,omitempty"`

	Bool bool `yaml:"bool,omitempty" json:"bool,omitempty"`

	Ledger *PreemptLedger `yaml:"ledger,omitempty" json:"ledger,omitempty"`

	Stranded []string `yaml:"stranded,omitempty" json:"stranded,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

#ArbiterInvokeReply is the action-multiplexed reply from verb:arbiter.

type ArtifactRef added in v0.2026210.2308

type ArtifactRef struct {
	ContainerPath string // path inside the builder stage/container
	HostPath      string // path on the deploy target (host fs or final-image fs)
	Chown         bool   // whether to re-own to the target user
}

ArtifactRef describes an output path from a builder that needs to be extracted back to the host or copied into the final OCI image.

For user-scope pixi/npm/cargo on the host target: the host path is bind-mounted into the builder, so there's no "extraction" — the Artifacts list is empty because the bind-mount already put the files in place. For aur: the list contains /tmp/aur-pkgs/*.pkg.tar.zst, which the host target pulls out to a staging dir then hands to pacman.

type ArtifactView

type ArtifactView struct {
	ContainerPath string `yaml:"container_path,omitempty" json:"container_path,omitempty"`

	HostPath string `yaml:"host_path,omitempty" json:"host_path,omitempty"`

	Chown bool `yaml:"chown,omitempty" json:"chown,omitempty"`
}

#ArtifactView is the wire mirror of package main's ArtifactRef.

type AutoLogin added in v0.2026242.935

type AutoLogin struct {
	User string `yaml:"user,omitempty" json:"user"`

	// relogin controls whether the greeter logs the user back in after they log out.
	// Default false: an operator who logs out usually means it.
	Relogin bool `yaml:"relogin,omitempty" json:"relogin,omitempty"`
}

type BakedLabelSet

type BakedLabelSet struct {
	// always-present scalars (the formatter emits these unconditionally as LABEL %q).
	Version string `yaml:"version,omitempty" json:"version"`

	Box string `yaml:"box,omitempty" json:"box"`

	UID int `yaml:"uid,omitempty" json:"uid"`

	GID int `yaml:"gid,omitempty" json:"gid"`

	User string `yaml:"user,omitempty" json:"user"`

	Home string `yaml:"home,omitempty" json:"home"`

	// conditional scalars (the formatter omits these when empty).
	Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`

	Bootc bool `yaml:"bootc,omitempty" json:"bootc,omitempty"`

	OCILabels map[string]string `yaml:"oci_labels,omitempty" json:"oci_labels,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	// platform identity + builder-pool coordination (writeJSONLabel).
	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`

	BuildFormat []string `yaml:"build_format,omitempty" json:"build_format,omitempty"`

	Builder map[string]string `yaml:"builder,omitempty" json:"builder,omitempty"`

	Build []string `yaml:"build,omitempty" json:"build,omitempty"`

	// JSON labels (omitted when empty). env is the image-level env MAP (the label wire form).
	Port []string `yaml:"port,omitempty" json:"port,omitempty"`

	PortProto map[string]string `yaml:"port_proto,omitempty" json:"port_proto,omitempty"`

	Volume []LabelVolumeEntry `yaml:"volume,omitempty" json:"volume,omitempty"`

	Alias []CollectedAlias `yaml:"alias,omitempty" json:"alias,omitempty"`

	Security Security `yaml:"security,omitempty" json:"security,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Hook *CandyHook `yaml:"hook,omitempty" json:"hook,omitempty"`

	Description *LabelDescriptionSet `yaml:"description,omitempty" json:"description,omitempty"`

	Shell *LabelShellSet `yaml:"shell,omitempty" json:"shell,omitempty"`

	// init system: active name + the deploy-relevant init def + the DYNAMIC label key the
	// per-init service-name list emits under (writeJSONLabel(b, labelInitDef.LabelKey, names)).
	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	InitDef *CapabilityInitDef `yaml:"init_def,omitempty" json:"init_def,omitempty"`

	InitLabelKey string `yaml:"init_label_key,omitempty" json:"init_label_key,omitempty"`

	ServiceNames []string `yaml:"service_names,omitempty" json:"service_names,omitempty"`

	Service []CapabilityService `yaml:"service,omitempty" json:"service,omitempty"`

	PortRelay []int `yaml:"port_relay,omitempty" json:"port_relay,omitempty"`

	Skills []LabelSkillEntry `yaml:"skills,omitempty" json:"skills,omitempty"`

	Secret []LabelSecretEntry `yaml:"secret,omitempty" json:"secret,omitempty"`

	EnvProvide map[string]string `yaml:"env_provide,omitempty" json:"env_provide,omitempty"`

	EnvRequire []EnvDependency `yaml:"env_require,omitempty" json:"env_require,omitempty"`

	EnvAccept []EnvDependency `yaml:"env_accept,omitempty" json:"env_accept,omitempty"`

	SecretRequire []EnvDependency `yaml:"secret_require,omitempty" json:"secret_require,omitempty"`

	SecretAccept []EnvDependency `yaml:"secret_accept,omitempty" json:"secret_accept,omitempty"`

	MCPProvide []CandyMCPProvide `yaml:"mcp_provide,omitempty" json:"mcp_provide,omitempty"`

	AgentProvide []AgentRuntimeCapability `yaml:"agent_provide,omitempty" json:"agent_provide,omitempty"`

	TerminalProfiles map[string]TerminalProfile `yaml:"terminal_profiles,omitempty" json:"terminal_profiles,omitempty"`

	MCPRequire []EnvDependency `yaml:"mcp_require,omitempty" json:"mcp_require,omitempty"`

	MCPAccept []EnvDependency `yaml:"mcp_accept,omitempty" json:"mcp_accept,omitempty"`

	Route []LabelRouteEntry `yaml:"route,omitempty" json:"route,omitempty"`

	EnvCandy map[string]string `yaml:"env_candy,omitempty" json:"env_candy,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	CheckLevel string `yaml:"check_level,omitempty" json:"check_level,omitempty"`

	Info string `yaml:"info,omitempty" json:"info,omitempty"`

	CandyVersion map[string]string `yaml:"candy_version,omitempty" json:"candy_version,omitempty"`

	DataEntries []LabelDataEntry `yaml:"data_entries,omitempty" json:"data_entries,omitempty"`

	DataImage bool `yaml:"data_image,omitempty" json:"data_image,omitempty"`
}

#BakedLabelSet — the BUILD-side wire-form carrier for the render's OCI-label emission (#67, the build_resolve RENDER-leg death). The former charly/generate.go writeLabels body split at the data/format boundary: the HOST gather (buildBakedMetadata) reads the live *Candy/*Config graph + the Collect* aggregators and produces a *spec.BakedLabelSet holding EXACTLY the data writeLabels emits, in WIRE form; the deploykit FORMATTER (WriteLabels) emits the LABEL lines from it byte-for-byte WITHOUT the live graph. It is distinct from #BoxMetadata (the DEPLOY-side hub ExtractMetadata builds field-by-field from labels): a few fields differ in shape between the bake wire form and the deploy read form — env is a map here (the label bakes a JSON object) but []string KEY=VALUE pairs in #BoxMetadata, and volume is #LabelVolumeEntry here (name+path) but #VolumeMount in #BoxMetadata — so the build carrier is its own wire-faithful type, never the deploy hub. It rides the envelope on #ResolvedBoxView.baked_metadata (build-render projection only) and re-attaches via NewSpecResolvedBox. MARSHALED (crosses core→plugin), so its tags are wire-relevant; the sub-shapes are the same CUE-sourced label types writeLabels emitted.

type BaseChainNode

type BaseChainNode struct {
	Name string
	Img  BoxConfig
}

BaseChainNode is one image visited while walking an internal base chain. Name is the ref as it was reached (bare for a root image, namespace-qualified for a base reached across an import boundary, e.g. `cachyos.cachyos`).

type BaseUser

type BaseUser struct {
	Name string `yaml:"name,omitempty" json:"name"`

	UID int `yaml:"uid,omitempty" json:"uid"`

	GID int `yaml:"gid,omitempty" json:"gid"`

	Home string `yaml:"home,omitempty" json:"home"`
}

type Bootloader

type Bootloader struct {
	InstallTemplate string `yaml:"install_template,omitempty" json:"install_template,omitempty"`

	InitramfsTemplate string `yaml:"initramfs_template,omitempty" json:"initramfs_template,omitempty"`

	FstabTemplate string `yaml:"fstab_template,omitempty" json:"fstab_template,omitempty"`
}

type Bootstrap

type Bootstrap struct {
	InstallCmd string `yaml:"install_cmd,omitempty" json:"install_cmd"`

	Package []string `yaml:"package,omitempty" json:"package,omitempty"`

	CacheMount []CacheMount `yaml:"cache_mount,omitempty" json:"cache_mount,omitempty"`
}

install_cmd is the bootstrap command; ubuntu sets it to "" (kept WITHOUT `& !=""` so the empty-string base case validates).

type Box

type Box struct {
	Name EntityRef `yaml:"name,omitempty" json:"name,omitempty"`

	Version CalVer `yaml:"version,omitempty" json:"version,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`

	Base string `yaml:"base,omitempty" json:"base,omitempty"`

	From string `yaml:"from,omitempty" json:"from,omitempty"`

	BootstrapBuilderImage string `yaml:"bootstrap_builder_image,omitempty" json:"bootstrap_builder_image,omitempty"`

	Platforms []string `yaml:"platform,omitempty" json:"platform,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`

	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`

	// BuildFormats: a scalar OR a list on the wire (#BuildFormats UnmarshalYAML
	// normalizes "rpm" → ["rpm"]); the Go field is []string. gengotypes degrades
	// the disjunction to `any`, so pin the Go shape explicitly (the typed enum is
	// a future enhancement — collapse for now).
	Build []string `yaml:"build,omitempty" json:"build,omitempty"`

	Candy []CandyRef `yaml:"candy,omitempty" json:"candy,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	UID *int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID *int `yaml:"gid,omitempty" json:"gid,omitempty"`

	UserPolicy string `yaml:"user_policy,omitempty" json:"user_policy,omitempty"`

	Builder map[string]string `yaml:"builder,omitempty" json:"builder,omitempty"`

	Produce []BuildType `yaml:"produce,omitempty" json:"produce,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	// init — pin the box's init system instead of letting composition auto-detect it.
	// Every value here must be an init the build vocabulary actually defines
	// (charly.yml's `init:` entities): a word the vocabulary knows but this enum
	// omits is unselectable, which is what kept `openrc` out of reach despite being
	// a first-class init with its own service_template, management commands and
	// unit_path_template.
	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	DataImage bool `yaml:"data_image,omitempty" json:"data_image,omitempty"`

	Bootc bool `yaml:"bootc,omitempty" json:"bootc,omitempty"`

	Readiness *Readiness `yaml:"readiness,omitempty" json:"readiness,omitempty"`

	// OCI entrypoint/cmd baked into the image's OCI config (emitted as the final
	// Containerfile ENTRYPOINT/CMD directives). OPT-IN and empty by default: a normal
	// charly image bakes no command and the deploy-time init (supervisord/systemd)
	// is injected instead. Declare these ONLY when something spawns the image
	// directly from its baked OCI config with no charly deploy in the loop — e.g. an
	// embedded controller (like AgentTeams) spawning child manager/worker containers
	// through a runtime socket. `cmd: []` clears the base image's inherited default
	// command; a declared entrypoint with no `cmd:` is auto-cleared at emission, so
	// the entrypoint runs bare.
	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"`

	Jobs *int `yaml:"jobs,omitempty" json:"jobs,omitempty"`

	PodmanJobs *int `yaml:"podman_jobs,omitempty" json:"podman_jobs,omitempty"`

	PodmanJobsCap *int `yaml:"podman_jobs_cap,omitempty" json:"podman_jobs_cap,omitempty"`

	ContextIgnore []string `yaml:"context_ignore,omitempty" json:"context_ignore,omitempty"`

	Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`

	KeepImages *int `yaml:"keep_images,omitempty" json:"keep_images,omitempty"`

	KeepCheckRuns *int `yaml:"keep_check_runs,omitempty" json:"keep_check_runs,omitempty"`

	Merge *BoxMerge `yaml:"merge,omitempty" json:"merge,omitempty"`

	Alias []BoxAlias `yaml:"alias,omitempty" json:"alias,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	CheckLevel string `yaml:"check_level,omitempty" json:"check_level,omitempty"`

	Shell *Shell `yaml:"shell,omitempty" json:"shell,omitempty"`
}

base and from are mutually exclusive; neither is also valid (scratch box). The entity-level base⊻from mutual-exclusion is enforced in GO (BoxConfig.HasBaseFromConflict, surfaced by validateBoxBaseFrom in validate.go and resolveBase in config.go) — NOT by a trailing `& ({from?: _|_} | {base?: _|_})` disjunction. `cue exp gengotypes` collapses an entity-level top-level disjunction to an EMPTY `struct{}`, which would make spec.Box useless as a drop-in for BoxConfig; the plain closed struct below generates the full field set, and the Go rule restores the XOR. The struct stays CLOSED (no `...`) so an unknown key is still a typo.

func (*Box) HasBaseFromConflict

func (ic *Box) HasBaseFromConflict() bool

HasBaseFromConflict reports whether the box authored BOTH base: and from:.

func (*Box) IsEnabled

func (ic *Box) IsEnabled() bool

IsEnabled reports whether the box is enabled (default true when unset).

type BoxAlias

type BoxAlias struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Command string `yaml:"command,omitempty" json:"command,omitempty"`
}

AliasConfig (config.go). CLOSED. command defaults to name when omitted.

type BoxConfig

type BoxConfig = Box

--- top-level entity types ---

func BoxConfigFrom

func BoxConfigFrom(m BoxMap, name string) (BoxConfig, bool)

BoxConfigFrom decodes name's image config from a generic image map.

func DecodeBox

func DecodeBox(raw json.RawMessage) (BoxConfig, bool)

DecodeBox decodes one opaque image body into the authored BoxConfig.

type BoxFetchResolveReply

type BoxFetchResolveReply struct {
	Path string `yaml:"path,omitempty" json:"path,omitempty"`
}

type BoxFetchResolveRequest

type BoxFetchResolveRequest struct {
	Spec string `yaml:"spec,omitempty" json:"spec"`

	Refresh bool `yaml:"refresh,omitempty" json:"refresh,omitempty"`
}

#BoxFetchResolveRequest/#BoxFetchResolveReply — the "box-fetch-resolve" HostBuild seam behind candy/plugin-authoring's command:fetch/command:refresh (K3 build-tail tail, coneB-buildremnant): the former hidden core `__box-fetch`/`__box-refresh` reentries (charly/box_fetch_reentry.go, DELETED) are replaced by ONE generic host-builder wrapping the SAME host-coupled repo resolver (ResolveProjectRepo → EnsureRepoDownloaded: CHARLY_REPO_OVERRIDE + the registered refs-backend download dispatch + the command:migrate auto-migration) — none of which an sdk-only plugin can run itself. refresh=true additionally force-removes the spec's cache entry before resolving (the former BoxRefreshCmd body) so a stale cache re-clones.

type BoxMap

type BoxMap = map[string]json.RawMessage

BoxMap is the generic image map type — name → opaque marshaled BoxConfig. A plain alias of map[string]json.RawMessage, so it round-trips byte-identically with charly's own (still separately declared, for the UnifiedFile side) `boxMap` alias — both resolve to the SAME underlying type.

type BoxMerge

type BoxMerge struct {
	Auto bool `yaml:"auto,omitempty" json:"auto,omitempty"`

	MaxMB int `yaml:"max_mb,omitempty" json:"max_mb,omitempty"`

	MaxTotalMB int `yaml:"max_total_mb,omitempty" json:"max_total_mb,omitempty"`
}

MergeConfig (config.go). CLOSED.

type BoxMetadata

type BoxMetadata struct {
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`

	Bootc bool `yaml:"bootc,omitempty" json:"bootc,omitempty"`

	UID int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int `yaml:"gid,omitempty" json:"gid,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	Port []string `yaml:"port,omitempty" json:"port,omitempty"`

	Volume []VolumeMount `yaml:"volume,omitempty" json:"volume,omitempty"`

	Alias []CollectedAlias `yaml:"alias,omitempty" json:"alias,omitempty"`

	Security Security `yaml:"security,omitempty" json:"security,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	Tunnel *TunnelYAML `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`

	DNS string `yaml:"dns,omitempty" json:"dns,omitempty"`

	AcmeEmail string `yaml:"acme_email,omitempty" json:"acme_email,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	Hook *CandyHook `yaml:"hook,omitempty" json:"hook,omitempty"`

	Route []LabelRouteEntry `yaml:"route,omitempty" json:"route,omitempty"`

	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	InitDef *CapabilityInitDef `yaml:"init_def,omitempty" json:"init_def,omitempty"`

	Service []CapabilityService `yaml:"service,omitempty" json:"service,omitempty"`

	ServiceNames []string `yaml:"service_names,omitempty" json:"service_names,omitempty"`

	EnvCandy map[string]string `yaml:"env_candy,omitempty" json:"env_candy,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	PortProto map[string]string `yaml:"port_proto,omitempty" json:"port_proto,omitempty"`

	PortRelay []int `yaml:"port_relay,omitempty" json:"port_relay,omitempty"`

	Skills []LabelSkillEntry `yaml:"skills,omitempty" json:"skills,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	Info string `yaml:"info,omitempty" json:"info,omitempty"`

	CandyVersion map[string]string `yaml:"candy_version,omitempty" json:"candy_version,omitempty"`

	Secret []LabelSecretEntry `yaml:"secret,omitempty" json:"secret,omitempty"`

	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`

	BuildFormat []string `yaml:"build_format,omitempty" json:"build_format,omitempty"`

	Builder map[string]string `yaml:"builder,omitempty" json:"builder,omitempty"`

	Build []string `yaml:"build,omitempty" json:"build,omitempty"`

	DataEntries []LabelDataEntry `yaml:"data_entries,omitempty" json:"data_entries,omitempty"`

	DataImage bool `yaml:"data_image,omitempty" json:"data_image,omitempty"`

	EnvProvide map[string]string `yaml:"env_provide,omitempty" json:"env_provide,omitempty"`

	EnvRequire []EnvDependency `yaml:"env_require,omitempty" json:"env_require,omitempty"`

	EnvAccept []EnvDependency `yaml:"env_accept,omitempty" json:"env_accept,omitempty"`

	SecretAccept []EnvDependency `yaml:"secret_accept,omitempty" json:"secret_accept,omitempty"`

	SecretRequire []EnvDependency `yaml:"secret_require,omitempty" json:"secret_require,omitempty"`

	MCPProvide []CandyMCPProvide `yaml:"mcp_provide,omitempty" json:"mcp_provide,omitempty"`

	AgentProvide []AgentRuntimeCapability `yaml:"agent_provide,omitempty" json:"agent_provide,omitempty"`

	TerminalProfiles map[string]TerminalProfile `yaml:"terminal_profiles,omitempty" json:"terminal_profiles,omitempty"`

	MCPRequire []EnvDependency `yaml:"mcp_require,omitempty" json:"mcp_require,omitempty"`

	MCPAccept []EnvDependency `yaml:"mcp_accept,omitempty" json:"mcp_accept,omitempty"`

	Description *LabelDescriptionSet `yaml:"description,omitempty" json:"description,omitempty"`

	Shell *LabelShellSet `yaml:"shell,omitempty" json:"shell,omitempty"`

	CheckLevel string `yaml:"check_level,omitempty" json:"check_level,omitempty"`
}

#BoxMetadata — the OCI-label metadata hub. NEVER whole-marshaled (ExtractMetadata builds it field-by-field), so its own tags are wire-irrelevant. Deploy-only fields (Tunnel/DNS/ AcmeEmail/Engine) are fed by MergeDeployOntoMetadata, never baked. PortProto RESHAPED map[int]string → map[string]string (the WIRE was always string-keyed; the int-key was an ExtractMetadata-internal convenience — resolveProto + ExtractMetadata rewired to match).

type BoxRefResolveReply

type BoxRefResolveReply struct {
	// exists_ref / pull_ref is the fully-qualified registry ref to check for local
	// presence / hand to `podman pull`; "" when the identifier could not be
	// resolved (no project reachable, or an unknown short name).
	ExistsRef string `yaml:"exists_ref,omitempty" json:"exists_ref,omitempty"`

	PullRef string `yaml:"pull_ref,omitempty" json:"pull_ref,omitempty"`

	// build_fallback_short is the short name (bare or namespace-qualified, e.g.
	// "fedora.fedora-builder") `charly box build` should target when the pull
	// fails; "" when no local build-fallback is possible for this identifier.
	BuildFallbackShort string `yaml:"build_fallback_short,omitempty" json:"build_fallback_short,omitempty"`

	// produced_ref is the registry ref build_fallback_short resolves to today
	// (registry+name, tag-less) — used to tag-alias a pinned-tag input ref onto
	// the freshly built image.
	ProducedRef string `yaml:"produced_ref,omitempty" json:"produced_ref,omitempty"`
}

type BoxRefResolveRequest

type BoxRefResolveRequest struct {
	Image string `yaml:"image,omitempty" json:"image"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
}

#BoxRefResolveRequest / #BoxRefResolveReply — the "box-ref-resolve" HostBuild seam: resolve a short-name or full-ref image identifier against charly.yml.

type BuildEnsureReply

type BuildEnsureReply struct {
	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type BuildEnsureRequest

type BuildEnsureRequest struct {
	Image string `yaml:"image,omitempty" json:"image"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	BuildEngine string `yaml:"build_engine,omitempty" json:"build_engine,omitempty"`

	RunEngine string `yaml:"run_engine,omitempty" json:"run_engine,omitempty"`
}

#BuildEnsureRequest / #BuildEnsureReply — the build:ensure word's Invoke envelope: ensure an image is present in local podman storage, falling back to a local (or remote-cached) build when the identifier maps to a project charly.yml entry.

type BuildEnv

type BuildEnv struct {
	Distros []string `yaml:"distros,omitempty" json:"distros,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	DevLocalPkg bool `yaml:"dev_local_pkg,omitempty" json:"dev_local_pkg,omitempty"`

	ImageBuildDir string `yaml:"image_build_dir,omitempty" json:"image_build_dir,omitempty"`

	ContextRelPrefix string `yaml:"context_rel_prefix,omitempty" json:"context_rel_prefix,omitempty"`

	// A pod-overlay deploy's add_candy: refs (if any) — threaded so a HOST-COUPLED word's OpEmit
	// (candy/plugin-installstep's getGenerator) can widen ITS OWN "resolved-project" envelope
	// re-fetch the same way ScanAllCandyWithConfigOpts' ResolveOpts.ExtraCandyRefs already widens
	// the host's own overlay Generator (hostBuildOverlay). Without this, an add_candy candy the
	// overlay build correctly resolved host-side is absent from this plugin's INDEPENDENT
	// envelope fetch, and candyByName's REMOTE-candy fallback still misses (RCA'd K1-alpha
	// regression: check-addcandy-pod's overlay-deploy path, "candy not found").
	ExtraCandyRefs []string `yaml:"extra_candy_refs,omitempty" json:"extra_candy_refs,omitempty"`
}

#BuildEnv is the build-context descriptor the host puts in op.Env for an OpEmit Invoke at image-generation time: the image's distro tags + name, so a plugin can tailor its emitted Containerfile fragment per distro/arch.

type BuildFormat

type BuildFormat = string

--- build vocabulary enums (string) ---

type BuildPkgReply

type BuildPkgReply struct {
	Written []string `yaml:"written,omitempty" json:"written,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type BuildPkgRequest

type BuildPkgRequest struct {
	Format []string `yaml:"format,omitempty" json:"format,omitempty"`

	Candy string `yaml:"candy,omitempty" json:"candy,omitempty"`

	Out string `yaml:"out,omitempty" json:"out,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
}

#BuildPkgRequest / #BuildPkgReply — the build:pkg word's Invoke envelope: `charly box pkg`'s CLI→DRIVE envelope (mirrors #BuildRequest's shape, K3 build-tail move — the former hidden core `__box-pkg` reentry is DELETED, its body now runs plugin-side in candy/plugin-build).

type BuildReply

type BuildReply struct {
	Written []string `yaml:"written,omitempty" json:"written,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

#BuildReply is what a build:box / build:generate plugin echoes back from its HostBuild call.

type BuildRequest

type BuildRequest struct {
	Boxes []string `yaml:"boxes,omitempty" json:"boxes,omitempty"`

	ExtraCandyRefs []string `yaml:"extra_candy_refs,omitempty" json:"extra_candy_refs,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	IncludeDisabled bool `yaml:"include_disabled,omitempty" json:"include_disabled,omitempty"`

	DevLocalPkg bool `yaml:"dev_local_pkg,omitempty" json:"dev_local_pkg,omitempty"`

	Push bool `yaml:"push,omitempty" json:"push,omitempty"`

	Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`

	Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`

	NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"`

	Jobs int `yaml:"jobs,omitempty" json:"jobs,omitempty"`

	PodmanJobs int `yaml:"podman_jobs,omitempty" json:"podman_jobs,omitempty"`
}

#BuildRequest is the CLI→DRIVE envelope: what `charly box build` / `charly box generate` marshal into the compiled-in candy/plugin-build's Invoke (op.Params).

type BuildResolveBox

type BuildResolveBox struct {
	Name string `yaml:"name,omitempty" json:"name"`

	FullTag string `yaml:"full_tag,omitempty" json:"full_tag,omitempty"`

	Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`

	Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"`

	MergeAuto bool `yaml:"merge_auto,omitempty" json:"merge_auto,omitempty"`

	MergeMaxMB int64 `yaml:"merge_max_mb,omitempty" json:"merge_max_mb,omitempty"`

	MergeMaxTotalMB int64 `yaml:"merge_max_total_mb,omitempty" json:"merge_max_total_mb,omitempty"`

	// from/bootstrap_builder_image/distro_def/bootstrap_builder are the privileged-bootstrap
	// inputs (a `from: builder:<name>` image) the candy needs to run
	// buildkit.RunPrivileged itself instead of the host doing it in the buildengine-prep seam.
	// bootstrap_builder is the SPECIFIC resolved #Builder for `from`'s builder name (not the
	// whole per-image builder map) — the minimal slice runPrivilegedBootstrap actually reads.
	// Both nil/empty for a non-bootstrap image (the common case).
	From string `yaml:"from,omitempty" json:"from,omitempty"`

	BootstrapBuilderImage string `yaml:"bootstrap_builder_image,omitempty" json:"bootstrap_builder_image,omitempty"`

	DistroDef *ResolvedDistro `yaml:"distro_def,omitempty" json:"distro_def,omitempty"`

	BootstrapBuilder *Builder `yaml:"bootstrap_builder,omitempty" json:"bootstrap_builder,omitempty"`
}

#BuildResolveBox is one image's drive descriptor: the tag to build and whether merge.auto fires for it (the candy gates the verb:oci layer-merge call on this bool; the host merge seam resolves the size knobs from config). The rendered Containerfile CONTENT is NO LONGER shipped in the reply (#67 render-DRIVE move): plugin-build renders Containerfiles itself from the resolved-project envelope (BuildResolveReply.resolved_project) via deploykit.Generator, so the candy pipes dg.Containerfiles[name] to podman — not a reply field.

type BuildResolveReply

type BuildResolveReply struct {
	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	EngineName string `yaml:"engine_name,omitempty" json:"engine_name,omitempty"`

	Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`

	Order []string `yaml:"order,omitempty" json:"order,omitempty"`

	Levels [][]string `yaml:"levels,omitempty" json:"levels,omitempty"`

	Boxes []BuildResolveBox `yaml:"boxes,omitempty" json:"boxes,omitempty"`

	Jobs int64 `yaml:"jobs,omitempty" json:"jobs,omitempty"`

	PodmanJobs int64 `yaml:"podman_jobs,omitempty" json:"podman_jobs,omitempty"`

	Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`

	KeepImages int64 `yaml:"keep_images,omitempty" json:"keep_images,omitempty"`

	Written []string `yaml:"written,omitempty" json:"written,omitempty"`

	// resolved_project — the full resolved-project envelope with build-render
	// caches (BakedMetadata/RenderCandyOrder/InitSystem/InitDef/ActiveInits/Caps
	// on each ResolvedBoxView + GlobalOrder/ExternalizedBuilders on the project),
	// so plugin-build renders Containerfiles via deploykit.Generator WITHOUT the
	// live *Candy/*Config graph (#67 render-DRIVE move). Filled by the buildengine-prep
	// seam (render-prep → projectResolvedProject with caches). Empty for the
	// generate-only path that writes Containerfiles host-side (transitional).
	ResolvedProject *ResolvedProject `yaml:"resolved_project,omitempty" json:"resolved_project,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type BuildResolveRequest

type BuildResolveRequest struct {
	Boxes []string `yaml:"boxes,omitempty" json:"boxes,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	IncludeDisabled bool `yaml:"include_disabled,omitempty" json:"include_disabled,omitempty"`

	DevLocalPkg bool `yaml:"dev_local_pkg,omitempty" json:"dev_local_pkg,omitempty"`

	Push bool `yaml:"push,omitempty" json:"push,omitempty"`

	Platform string `yaml:"platform,omitempty" json:"platform,omitempty"`

	Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`

	NoCache bool `yaml:"no_cache,omitempty" json:"no_cache,omitempty"`

	Jobs int64 `yaml:"jobs,omitempty" json:"jobs,omitempty"`

	PodmanJobs int64 `yaml:"podman_jobs,omitempty" json:"podman_jobs,omitempty"`

	GenerateOnly bool `yaml:"generate_only,omitempty" json:"generate_only,omitempty"`
}

#BuildResolveRequest carries the CLI-supplied build inputs (the former BuildRequest fields) the host cannot reconstruct from Dir alone. The host runs NewGenerator (loader) + Generate (render → .build/), the privileged builder-bootstrap, and the builder-image ensure host-side, then returns the drive-model. GenerateOnly (the `charly box generate` path) renders + returns the written Containerfile paths WITHOUT bootstrap/ensure/buildengine-prep.

type BuildResolvedBox added in v0.2026215.1748

type BuildResolvedBox struct {
	ResolvedBox

	// Build config (resolved per-image via charly.yml import: + the binary-embedded build vocabulary)
	DistroConfig  *DistroConfig   `json:"-"`
	DistroDef     *ResolvedDistro `json:"-"`
	BuilderConfig *BuilderConfig  `json:"-"`
	InitSystem    string          `json:"-"`
	InitDef       *ResolvedInit   `json:"-"`

	CandyCaps *AggregatedCandyCaps `json:"-"`

	BakedMetadata    *BakedLabelSet           `json:"-"`
	RenderCandyOrder []string                 `json:"-"`
	ActiveInits      map[string]*ResolvedInit `json:"-"`
}

build_resolved_box.go — SPIKE: the render-time embedding wrapper relocated from sdk/buildkit/resolved_box.go (#55 value-type relocation spike). Every field type here already resolves to a spec.* type (DistroConfig/BuilderConfig/ResolvedDistro/ ResolvedInit/AggregatedCandyCaps/BakedLabelSet are all CUE-generated in this same package) so the wrapper carries ZERO buildkit-only content — it exists purely because gengotypes has no json:"-" construct (the kit.CheckResult/DeadlineExceeded pattern — see /charly-internals:go "Generation coverage"). buildkit.ResolvedBox becomes a type alias onto this type so every existing buildkit/deploykit/candy call site compiles unchanged.

type BuildStageContext

type BuildStageContext struct {
	BuilderRef string `yaml:"builder_ref,omitempty" json:"builder_ref,omitempty"`

	StageName string `yaml:"stage_name,omitempty" json:"stage_name,omitempty"`

	LayerStage string `yaml:"layer_stage,omitempty" json:"layer_stage,omitempty"`

	CopySrc string `yaml:"copy_src,omitempty" json:"copy_src,omitempty"`

	UID int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int `yaml:"gid,omitempty" json:"gid,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	Manifest string `yaml:"manifest,omitempty" json:"manifest,omitempty"`

	HasLockFile bool `yaml:"has_lock_file,omitempty" json:"has_lock_file,omitempty"`

	InstallCmd string `yaml:"install_cmd,omitempty" json:"install_cmd,omitempty"`

	ManylinuxFix string `yaml:"manylinux_fix,omitempty" json:"manylinux_fix,omitempty"`

	CacheMounts []CacheMount `yaml:"cache_mounts,omitempty" json:"cache_mounts,omitempty"`

	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Options []string `yaml:"options,omitempty" json:"options,omitempty"`

	HasBuildScript bool `yaml:"has_build_script,omitempty" json:"has_build_script,omitempty"`

	BuildScript string `yaml:"build_script,omitempty" json:"build_script,omitempty"`
}

#BuildStageContext — data a builder's multi-stage `stage_template` renders against: builder/stage identity, scratch-stage + copy-src wiring, detected manifest/lockfile/build-script, install command, cache mounts, and (for config-detected builders like aur) packages/options.

type BuildTarget

type BuildTarget struct {
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Auto bool `yaml:"auto,omitempty" json:"auto,omitempty"`
}

#BuildTarget — a build-order entry (charly box list targets): the box/intermediate name + whether it is an auto-computed intermediate (the `name [auto]` output).

type BuildType

type BuildType = string

--- build vocabulary enums (string) ---

type Builder

type Builder struct {
	DetectFiles []string `yaml:"detect_file,omitempty" json:"detect_file,omitempty"`

	DetectConfig string `yaml:"detect_config,omitempty" json:"detect_config,omitempty"`

	RequiresSrcDir bool `yaml:"requires_src_dir,omitempty" json:"requires_src_dir,omitempty"`

	Inline bool `yaml:"inline,omitempty" json:"inline,omitempty"`

	CacheMount []CacheMount `yaml:"cache_mount,omitempty" json:"cache_mount,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	RuntimeEnv StrMap `yaml:"runtime_env,omitempty" json:"runtime_env,omitempty"`

	ManylinuxFix string `yaml:"manylinux_fix,omitempty" json:"manylinux_fix,omitempty"`

	BuildScript string `yaml:"build_script,omitempty" json:"build_script,omitempty"`

	Phases *PhaseSet `yaml:"phase,omitempty" json:"phase,omitempty"`

	InstallCommands map[string]string `yaml:"install_command,omitempty" json:"install_command,omitempty"`

	CopyArtifacts []Copy `yaml:"copy_artifact,omitempty" json:"copy_artifact,omitempty"`

	CopyBinary *Copy `yaml:"copy_binary,omitempty" json:"copy_binary,omitempty"`

	PathContributions []string `yaml:"path_contribution,omitempty" json:"path_contribution,omitempty"`

	Kind string `yaml:"kind,omitempty" json:"kind"`

	Privileged bool `yaml:"privileged,omitempty" json:"privileged"`

	OutputArtifact string `yaml:"output_artifact,omitempty" json:"output_artifact,omitempty"`
}

func (*Builder) IsBootstrap

func (b *Builder) IsBootstrap() bool

IsBootstrap reports whether the builder is a bootstrap builder.

type BuilderCollectInput

type BuilderCollectInput struct {
	Candy string `yaml:"candy,omitempty" json:"candy"`

	Builder string `yaml:"builder,omitempty" json:"builder"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Replaces []string `yaml:"replaces,omitempty" json:"replaces,omitempty"`
}

#BuilderCollectInput is the OpCollectContext params: the host-supplied candy descriptor an external builder plugin reads to produce its per-candy stage context.

type BuilderCollectReply

type BuilderCollectReply struct {
	Context map[string]any `yaml:"context,omitempty" json:"context,omitempty"`
}

#BuilderCollectReply is the OpCollectContext reply: the builder-specific stage-context keys the host merges onto the base context.

type BuilderConfig added in v0.2026210.2052

type BuilderConfig struct {
	Builder map[string]*Builder `yaml:"builder,omitempty" json:"builder"`
}

#BuilderConfig is the `builder:` build-vocabulary container — the whole builder map keyed by builder name (charly/charly.yml's `builder:` section). #55 step 3-III: relocated from sdk/buildkit for the import-purity lever (see #DistroConfig). Its methods (ValidBuilderType / BuilderNames) are pure Go METHODS and stay hand-written in spec/builder_config_methods.go.

func ProjectBuilderConfig added in v0.2026213.1726

func ProjectBuilderConfig(uf *UnifiedFile) *BuilderConfig

ProjectBuilderConfig reconstructs the *BuilderConfig (builder: section) from uf. The builder bodies decode purely (DecodePluginKindMap) — no OpResolve callback needed. Nil when no builders are configured.

func (*BuilderConfig) BuilderNames added in v0.2026210.2052

func (bc *BuilderConfig) BuilderNames() []string

BuilderNames returns sorted list of defined builder names.

func (*BuilderConfig) ValidBuilderType added in v0.2026210.2052

func (bc *BuilderConfig) ValidBuilderType(name string) bool

ValidBuilderType returns true if the given name is a defined builder.

type BuilderDef

type BuilderDef = Builder

--- top-level entity types ---

type BuilderMap added in v0.2026210.1533

type BuilderMap map[string]string

#BuilderMap — a map of build type (pixi/npm/cargo/aur) → builder image name.

func EffectiveBuilderForBox added in v0.2026213.1726

func EffectiveBuilderForBox(cfg *Config, name string, img BoxConfig) BuilderMap

EffectiveBuilderForBox computes the builder image refs an image will build against, from a RAW BoxConfig — the FETCH-path counterpart to ResolveBox's resolved-value path. Both end at the ONE canonical ResolveEffectiveBuilder.

func PickDistroBuilder added in v0.2026213.1726

func PickDistroBuilder(candidates []DistroBuilderCandidate, distroTags []string) BuilderMap

PickDistroBuilder returns the builder map of the first candidate (in the given, caller-sorted order — determinism is the caller's responsibility, so the result is stable when more than one candidate shares a distro tag) whose Distro contains a tag, walking distroTags in priority order (most specific first, e.g. ["cachyos","arch"] or ["fedora:43","fedora"]). Only candidates with a non-empty Builder are considered.

func ResolveEffectiveBuilder added in v0.2026213.1726

func ResolveEffectiveBuilder(cfg *Config, name string, distro []string, base string, isExternalBase bool, imgBuilder BuilderMap) BuilderMap

ResolveEffectiveBuilder computes an image's effective builder map via the SINGLE canonical precedence, lowest→highest: defaults.builder → distro-keyed default → direct local base → per-image override — then self-references are filtered. EVERY builder-consuming path calls this so resolution can never drift between commands.

func (BuilderMap) AllBuilder added in v0.2026210.1533

func (m BuilderMap) AllBuilder() []string

AllBuilder returns a deduplicated sorted list of builder image names.

func (BuilderMap) BuilderFor added in v0.2026210.1533

func (m BuilderMap) BuilderFor(format string) string

BuilderFor returns the builder image name for the given format, or "".

func (BuilderMap) HasBuilder added in v0.2026210.1533

func (m BuilderMap) HasBuilder(format string) bool

HasBuilder returns true if a builder is configured for the given format.

type BuilderPreresolved added in v0.2026213.1726

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

BuilderPreresolved is one candy×builder's pre-resolved payload: the builder-specific render Context (opaque key/value map from the builder plugin's OpCollectContext) plus its Reverse teardown ops (from OpReverse). Consumed by the deploy-plan compiler, which only reads it.

type BuilderResolveInput

type BuilderResolveInput struct {
	Candy string `yaml:"candy,omitempty" json:"candy"`

	Builder string `yaml:"builder,omitempty" json:"builder,omitempty"`

	BuilderRef string `yaml:"builder_ref,omitempty" json:"builder_ref,omitempty"`

	StageName string `yaml:"stage_name,omitempty" json:"stage_name,omitempty"`

	LayerStage string `yaml:"layer_stage,omitempty" json:"layer_stage,omitempty"`

	CopySrc string `yaml:"copy_src,omitempty" json:"copy_src,omitempty"`

	UID int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int `yaml:"gid,omitempty" json:"gid,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	Manifest string `yaml:"manifest,omitempty" json:"manifest,omitempty"`

	HasLockFile bool `yaml:"has_lock_file,omitempty" json:"has_lock_file,omitempty"`

	InstallCmd string `yaml:"install_cmd,omitempty" json:"install_cmd,omitempty"`

	ManylinuxFix string `yaml:"manylinux_fix,omitempty" json:"manylinux_fix,omitempty"`

	HasBuildScript bool `yaml:"has_build_script,omitempty" json:"has_build_script,omitempty"`

	BuildScript string `yaml:"build_script,omitempty" json:"build_script,omitempty"`

	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Options []string `yaml:"options,omitempty" json:"options,omitempty"`

	CacheMountsOwned string `yaml:"cache_mounts_owned,omitempty" json:"cache_mounts_owned,omitempty"`

	CacheMountsAuto string `yaml:"cache_mounts_auto,omitempty" json:"cache_mounts_auto,omitempty"`

	Inline bool `yaml:"inline,omitempty" json:"inline,omitempty"`
}

#BuilderResolveInput is the OpResolve params: the RENDER CONTEXT the host computes and hands a builder plugin so it can render its build-time multi-stage self-contained.

func BuilderResolveInputFrom added in v0.2026213.1726

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

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

type BuilderResolveReply

type BuilderResolveReply struct {
	Stage string `yaml:"stage,omitempty" json:"stage,omitempty"`

	CopyArtifacts []string `yaml:"copy_artifacts,omitempty" json:"copy_artifacts,omitempty"`

	CopyBinary string `yaml:"copy_binary,omitempty" json:"copy_binary,omitempty"`

	InlineFragment string `yaml:"inline_fragment,omitempty" json:"inline_fragment,omitempty"`
}

#BuilderResolveReply is what a builder plugin returns from an OpResolve Invoke at image-generation time — the build-time BUILDER leg.

type BuilderReverseInput

type BuilderReverseInput struct {
	Candy string `yaml:"candy,omitempty" json:"candy"`

	Builder string `yaml:"builder,omitempty" json:"builder"`

	Context map[string]any `yaml:"context,omitempty" json:"context,omitempty"`
}

#BuilderReverseInput is the OpReverse params: the candy + its resolved stage-context keys.

type BuilderReverseReply

type BuilderReverseReply struct {
	ReverseOps []ReverseOp `yaml:"reverse_ops,omitempty" json:"reverse_ops,omitempty"`
}

#BuilderReverseReply is the OpReverse reply: the builder's teardown ops.

type BuilderRunOpts

type BuilderRunOpts struct {
	Engine       string // "podman" or "docker"; default "podman"
	BuilderImage string // full image ref, e.g. "ghcr.io/opencharly/fedora-builder:latest"
	CandyDir     string // absolute path to candy source (bind-mounted as /work)
	ScriptBody   string // shell script contents to pass to bash -c

	// ResolveImage + EnsureImage inject the image-presence behavior that used to
	// read *Config directly (Config stays a charly kernel type; the executor is an
	// sdk mechanism reached through this seam). ResolveImage maps a short/namespaced
	// builder ref to its concrete podman storage key (side-effect-free — safe in the
	// dry-run path). EnsureImage guarantees the image is present, falling back to a
	// local `charly box build <basename>` when it is project-buildable. The charly
	// callers close over Cfg + ProjectDir; see charly/host_build_box_ref_resolve.go
	// (ResolveImage) and charly/dispatch_build_ensure.go (EnsureImage).
	ResolveImage func(image string) (string, error)
	EnsureImage  func(ctx context.Context, image string) error

	// Bind-mounts. Keys are container paths; values are host paths.
	// Set by the caller based on the builder kind — pixi/npm/cargo use
	// the same HOME-subdir layout, aur uses a tmpdir for /tmp/aur-pkgs.
	BindMounts map[string]string

	// Env vars to set inside the container (in addition to HOME).
	Env map[string]string

	// HostHome is the invoking user's absolute home dir. Set via HOME=
	// inside the container so path-baking (pixi shebangs, etc.) resolves
	// to a path that's valid both inside (via bind-mount) and outside.
	HostHome string

	// DryRun returns the command line that would run without executing.
	// Used for --dry-run deploy.
	DryRun bool

	// RunAsRoot spawns the container as UID 0 instead of the host's
	// UID. Needed for builders whose script body uses `sudo` against
	// users that don't exist in the builder image's /etc/passwd —
	// e.g. AUR's makepkg+yay flow inside a non-OCI-staged builder
	// image. Under rootless podman, root-in-container maps to the
	// host's UID, so file ownership in bind-mounts stays correct.
	RunAsRoot bool
}

BuilderRunOpts describes one `podman run <builder>` invocation. The BuilderRun implementation (host-engine builder exec) lives in sdk/kit; this options struct is spec-homed because DeployExecutor.RunBuilder carries it across the IR.

type BuilderStep added in v0.2026210.2308

type BuilderStep struct {
	Builder      string        // "pixi" | "npm" | "cargo" | "aur"
	BuilderImage string        // resolved image ref (e.g. "fedora-builder:2026.04")
	CandyName    string        // layer that triggered this builder (for ledger)
	CandyDir     string        // absolute layer source path (bind-mounted as /work)
	Phase        Phase         // typically PhaseInstall
	Artifacts    []ArtifactRef // outputs to extract (empty for user-scope pixi/npm/cargo; populated for aur)

	// Builder-specific template context — the compiler populates this from
	// the candy's manifest files + the embedded builder definition (charly/charly.yml).
	RawStageContext map[string]any

	// LocalPkg is the package format's localpkg contract, populated by the
	// compiler for the `aur` builder only. The aur builder produces package
	// files (.pkg.tar.zst) that the host/VM deploy targets install onto the
	// venue via the SAME config-driven transfer+install leg the localpkg step
	// uses (R3) — so the install command + package glob come from
	// the embedded `pac.local_pkg` vocabulary, never a hardcoded literal. Nil for
	// pixi/npm/cargo (home-artifact builders, no package-file install).
	LocalPkg *LocalPkg

	// BuilderDef is the resolved builder definition from the embedded build vocabulary (charly/charly.yml) for this builder
	// (img.BuilderConfig.Builder[Builder]), populated by the compiler. The
	// host-venue deploy targets render its phase.install.host cell via
	// renderBuilderScript — the plain-shell analog of the container stage — so the
	// host build script is config-driven, not hardcoded Go. nil only on
	// synthetic test paths that don't supply a BuilderConfig.
	BuilderDef *BuilderDef

	// PreResolvedReverse is the builder's teardown ops, resolved HOST-SIDE in the build
	// PRE-PASS (builder_preresolve.go → the externalized builder plugin's OpReverse) and stashed
	// here so Reverse() is a PURE getter — the per-builder reverse-op KIND (pixi-env-remove /
	// package-remove / …) is exactly the logic externalized out-of-process, and Reverse() runs
	// host-side (step-view projection + RunHostStep) with no registry handle. Carried across the
	// wire on InstallStepView.ReverseOps (the host-computed teardown field) and restored by
	// stepFromView. Nil for a custom candy builder / a direct compile with no pre-pass.
	PreResolvedReverse []ReverseOp
}

BuilderStep runs a multi-stage builder (pixi/npm/cargo/aur). On the OCI target this emits a FROM + RUN stage plus a final-stage COPY --from. On the host target this emits a `podman run <builder>` with HOME-remap and appropriate bind-mounts.

func (*BuilderStep) Kind added in v0.2026210.2308

func (s *BuilderStep) Kind() StepKind

func (*BuilderStep) RequiresGate added in v0.2026210.2308

func (s *BuilderStep) RequiresGate() Gate

func (*BuilderStep) Reverse added in v0.2026210.2308

func (s *BuilderStep) Reverse() []ReverseOp

func (*BuilderStep) Scope added in v0.2026210.2308

func (s *BuilderStep) Scope() Scope

func (*BuilderStep) Venue added in v0.2026210.2308

func (s *BuilderStep) Venue() Venue

type CLIArg

type CLIArg struct {
	Prop string `yaml:"prop,omitempty" json:"prop"`

	Name string `yaml:"name,omitempty" json:"name"`

	Type string `yaml:"type,omitempty" json:"type"`

	Help string `yaml:"help,omitempty" json:"help,omitempty"`

	Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"`

	Default string `yaml:"default,omitempty" json:"default,omitempty"`

	HasDefault bool `yaml:"has_default,omitempty" json:"has_default,omitempty"`

	Required bool `yaml:"required,omitempty" json:"required,omitempty"`

	IsBool bool `yaml:"is_bool,omitempty" json:"is_bool,omitempty"`

	IsSlice bool `yaml:"is_slice,omitempty" json:"is_slice,omitempty"`

	IsMap bool `yaml:"is_map,omitempty" json:"is_map,omitempty"`

	Negated bool `yaml:"negated,omitempty" json:"negated,omitempty"`

	ElemType string `yaml:"elem_type,omitempty" json:"elem_type,omitempty"`
}

CLI reflection is a wire contract between command plugins, the Charly host, and MCP. It is CUE-owned like every other authored or transported shape.

type CLILeaf

type CLILeaf struct {
	Path string `yaml:"path,omitempty" json:"path"`

	Help string `yaml:"help,omitempty" json:"help,omitempty"`

	Hidden bool `yaml:"hidden,omitempty" json:"hidden,omitempty"`

	Positionals []CLIArg `yaml:"positionals,omitempty" json:"positionals,omitempty"`

	Flags []CLIArg `yaml:"flags,omitempty" json:"flags,omitempty"`
}

type CLIModel

type CLIModel struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Version string `yaml:"version,omitempty" json:"version"`

	Leaves []CLILeaf `yaml:"leaves,omitempty" json:"leaves"`
}

type CacheConfig added in v0.2026241.931

type CacheConfig struct {
	Git GitCache `yaml:"git,omitempty" json:"git,omitempty"`
}

#CacheConfig is the top-level `cache:` block. `git` holds the git metadata cache; future caches (image layers, check runs) add their own sub-key.

type CacheMount

type CacheMount struct {
	Dst string `yaml:"dst,omitempty" json:"dst"`

	Sharing string `yaml:"sharing,omitempty" json:"sharing,omitempty"`

	Owned bool `yaml:"owned,omitempty" json:"owned,omitempty"`
}

A BuildKit cache mount. dst is the absolute in-builder cache path; sharing is the BuildKit sharing mode; owned renders a uid/gid-owned cache.

type CacheMountSpec added in v0.2026210.2308

type CacheMountSpec struct {
	Dst     string
	Sharing string // "locked" | "private" | "shared" | ""
}

CacheMountSpec mirrors the BuildKit cache-mount configuration carried in the embedded build vocabulary's format/builder definitions (charly/charly.yml). The OCI target renders these as `--mount=type=cache,...`; the host target ignores them.

type CacheMountView

type CacheMountView struct {
	Dst string `yaml:"dst,omitempty" json:"dst,omitempty"`

	Sharing string `yaml:"sharing,omitempty" json:"sharing,omitempty"`
}

#CacheMountView is the wire mirror of package main's CacheMountSpec.

type CalVer

type CalVer = string

--- named-pattern scalars (string) ---

type Candy

type Candy struct {
	// --- identity (required: ADE mandates version+name+description+plan) ---
	Version CalVer `yaml:"version,omitempty" json:"version"`

	Name EntityRef `yaml:"name,omitempty" json:"name,omitempty"`

	Description string `yaml:"description,omitempty" json:"description"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	// --- maturity / engine ---
	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	// engine — a candy's required run engine; per-image resolution walks the
	// candy chain (engine.go ResolveBoxEngine) and cross-candy conflicts are a
	// validate error. RETAINED by the schema-compaction consumer audit.
	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	// `from:` is NOT a candy field — EDGE-INHERIT cutover D: a candy: node carrying
	// `base:` or `from:` is a full IMAGE (#Box, the former box:), routed there by the
	// loader; a LAYER fragment has neither. So `from:` lives only on #Box.
	Reboot bool `yaml:"reboot,omitempty" json:"reboot,omitempty"`

	// --- composition ---
	Candy []CandyRef `yaml:"candy,omitempty" json:"candy,omitempty"`

	Require []CandyRef `yaml:"require,omitempty" json:"require,omitempty"`

	// bake_plugin — the OUT-OF-TREE plugin candies whose pre-built provider binary
	// this candy BAKES into every composing image at /usr/lib/charly/plugins/, so a
	// DEPLOYED container (no candy source, no go toolchain) can run an external plugin
	// its in-container charly needs at runtime — e.g. charly-mcp bakes plugin-mcp so
	// `charly mcp serve` resolves the external `mcp` command in-container. The S0
	// baked-plugin BUILD-side seam, the deploy-time counterpart of resolvePluginBinary's
	// bakedPluginBinary fallback (plugin_loader.go). See generate.go emitBakedPlugins.
	BakePlugin []CandyRef `yaml:"bake_plugin,omitempty" json:"bake_plugin,omitempty"`

	RequiresCapability []string `yaml:"requires_capability,omitempty" json:"requires_capability,omitempty"`

	Capability *CandyCapability `yaml:"capability,omitempty" json:"capability,omitempty"`

	// plugin — declaring this block makes the candy a PLUGIN: it provides
	// reserved-word Providers (built-in OR out-of-tree). The candy is otherwise
	// authored, validated, built, deployed, and checked like any candy (R3 — one
	// authoring surface). See provider.go / plugin_loader.go.
	Plugin *Plugin `yaml:"plugin,omitempty" json:"plugin,omitempty"`

	// external_builder — the reserved word of an EXTERNAL builder plugin
	// (`builder:<word>`, an out-of-tree grpcProvider) this candy SELECTS to produce
	// a multi-stage build artifact. At image build the generator resolves the word,
	// Invokes the provider's OpResolve, and splices the returned BuilderResolveReply
	// (the FROM…AS stage pre-main-FROM + the COPY --from artifacts post-main-FROM).
	// The build-time BUILDER leg — the counterpart of a `run:` step's `plugin:` verb
	// (the STEP leg). A builtin builder (pixi/cargo/npm/aur) is selected by detection
	// files, NOT this field. See generate.go emitExternalBuilderStages.
	ExternalBuilder string `yaml:"external_builder,omitempty" json:"external_builder,omitempty"`

	// --- runtime env / local vars / PATH ---
	// env forbids PATH (validate.go: use path_append instead). Values are
	// Go-coerced scalars (#StrVal) — an unquoted `PORT: 8080` is a string. The Go
	// field is map[string]string (yaml.v3 coerces the scalar), so pin the shape
	// (gengotypes degrades the #StrVal value to `any`).
	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	// var: Go field is `Vars map[string]string`; gengotypes degrades the
	// pattern-keyed map to an empty struct, so pin name + shape.
	Vars map[string]string `yaml:"var,omitempty" json:"var,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	// --- package surface (Calamares-aligned) ---
	Package []PackageItem `yaml:"package,omitempty" json:"package,omitempty"`

	Distro map[string]*DistroPackages `yaml:"distro,omitempty" json:"distro,omitempty"`

	Apk []ApkPackageSpec `yaml:"apk,omitempty" json:"apk,omitempty"`

	// packaging — the native-package metadata for this candy: the SINGLE source
	// of truth the `charly generate-packages` plugin (sdk/packagekit) reads to
	// build distro packages (deb/rpm/apk/archlinux/ipk/msix). Any candy may
	// declare it (kind-blind); the charly candy's charly.yml carries the real
	// section. Replaces the old `localpkg:` source-dir map — the plugin builds
	// from the released binary + plugins, not from a bundled source tree.
	Packaging *Packaging `yaml:"packaging,omitempty" json:"packaging,omitempty"`

	// --- networking / routing ---
	// PortSpec: a plain int OR a "proto:port" string (proto ∈ http/https/tcp/…);
	// the Go normalizer canonicalizes either form to the {port, protocol}
	// PortSpec struct, so the Go field is []PortSpec (gengotypes degrades the
	// scalar|string disjunction element to `any`).
	Port []PortSpec `yaml:"port,omitempty" json:"port,omitempty"`

	PortRelay []int `yaml:"port_relay,omitempty" json:"port_relay,omitempty"`

	Route *CandyRoute `yaml:"route,omitempty" json:"route,omitempty"`

	// --- services / volumes / aliases / extract / data ---
	Service []CandyService `yaml:"service,omitempty" json:"service,omitempty"`

	Volume []CandyVolume `yaml:"volume,omitempty" json:"volume,omitempty"`

	Alias []CandyAlias `yaml:"alias,omitempty" json:"alias,omitempty"`

	Extract []CandyExtract `yaml:"extract,omitempty" json:"extract,omitempty"`

	Data []CandyData `yaml:"data,omitempty" json:"data,omitempty"`

	// --- security / hooks ---
	// (the candy-level `libvirt:` raw-XML-snippet field was removed — zero live Go
	// consumers, retired alongside the box-level libvirt hard-cutover; migrated
	// away by candy/plugin-migrate's stripCandyLibvirtField. The paired `kind: vm`
	// entity's OWN `libvirt: {...}` domain config, sdk/schema/vm.cue #Vm.libvirt,
	// is unrelated and unaffected.)
	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	Hook *CandyHook `yaml:"hook,omitempty" json:"hook,omitempty"`

	// --- env/secret/mcp dependency + provides surface ---
	// All six env/secret/mcp dependency lists decode into the ONE Go type
	// []EnvDependency (pinned via @go(...,type=[]EnvDependency)); the per-context
	// CUE refinements (env-var name regex; the charly/ secret-key prefix) only
	// TIGHTEN validation — they don't change the emitted Go type (an inline
	// `#EnvDependency & {…}` would otherwise make gengotypes emit an anonymous
	// struct). mcp_* keep the base shape (an MCP server name is not env-var-
	// constrained).
	EnvProvides StrMap `yaml:"env_provide,omitempty" json:"env_provide,omitempty"`

	EnvRequire []EnvDependency `yaml:"env_require,omitempty" json:"env_require,omitempty"`

	EnvAccept []EnvDependency `yaml:"env_accept,omitempty" json:"env_accept,omitempty"`

	SecretAccept []EnvDependency `yaml:"secret_accept,omitempty" json:"secret_accept,omitempty"`

	SecretRequire []EnvDependency `yaml:"secret_require,omitempty" json:"secret_require,omitempty"`

	MCPProvide []CandyMCPProvide `yaml:"mcp_provide,omitempty" json:"mcp_provide,omitempty"`

	AgentProvide []AgentRuntimeCapability `yaml:"agent_provide,omitempty" json:"agent_provide,omitempty"`

	// terminal_profile keeps the authored-key convention: every collection-
	// holding authored key in this block is singular (env_provide, mcp_provide,
	// agent_provide, …). The baked wire/label shape is the PLURAL
	// `terminal_profiles` (schema/boxmetadata.cue; spec.LabelTerminalProfiles
	// "ai.opencharly.terminal_profiles") — a key burned into built images, so
	// the singular-authored ↔ plural-wire mapping is deliberate and stable,
	// not a typo to "align" (renaming either side is a format change).
	TerminalProfiles map[string]TerminalProfile `yaml:"terminal_profile,omitempty" json:"terminal_profile,omitempty"`

	MCPRequire []EnvDependency `yaml:"mcp_require,omitempty" json:"mcp_require,omitempty"`

	MCPAccept []EnvDependency `yaml:"mcp_accept,omitempty" json:"mcp_accept,omitempty"`

	SecretYAML []CandySecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	// --- shell init (same shape as box shell) ---
	Shell *Shell `yaml:"shell,omitempty" json:"shell,omitempty"`

	// --- operator-facing artifacts ---
	Artifact []CandyArtifact `yaml:"artifact,omitempty" json:"artifact,omitempty"`
}

type CandyAlias

type CandyAlias struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Command string `yaml:"command,omitempty" json:"command"`
}

AliasYAML — for a CANDY alias `command` is REQUIRED (validateAliases); the box-level #BoxAlias keeps it optional (defaults to name).

type CandyArtifact

type CandyArtifact struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Path string `yaml:"path,omitempty" json:"path"`

	RetrieveTo string `yaml:"retrieve_to,omitempty" json:"retrieve_to"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Optional bool `yaml:"optional,omitempty" json:"optional,omitempty"`

	WaitSeconds int `yaml:"wait_second,omitempty" json:"wait_second,omitempty"`

	Rewrite []CandyArtifactRewrite `yaml:"rewrite,omitempty" json:"rewrite,omitempty"`

	// register: an optional NAME-BLIND post-retrieve processing hint (e.g. "kubeconfig") —
	// what to do with the retrieved artifact beyond the generic retrieve/rewrite pipeline,
	// dispatched by a host-side registry keyed on this word (retrieveArtifactsAndK3s /
	// artifactRegisterHandlers). Replaces a hardcoded candy-NAME check
	// (deployHasCandy(candyList, "k3s-server")) with a declaration the candy itself carries,
	// so a future candy needing the same registration need not be named "k3s-server".
	Register string `yaml:"register,omitempty" json:"register,omitempty"`
}

CandyArtifact — a file the candy publishes back to the operator post-setup.

type CandyArtifactRewrite

type CandyArtifactRewrite struct {
	Find string `yaml:"find,omitempty" json:"find"`

	Replace string `yaml:"replace,omitempty" json:"replace,omitempty"`
}

type CandyCandidate added in v0.2026210.808

type CandyCandidate struct {
	Scanned ScannedCandy
	Version string // per-entity version (Scanned.Model.Version) — mandatory, never ""
	GitTag  string // fetch coordinate (the @github :vTAG)
	Source  string // "<repo>@<git-tag>" for warning attribution
}

CandyCandidate is one fetched materialization of a bare candy ref. The git tag is the fetch coordinate; Version is the candy's own per-entity `version:`. Relocated from loaderkit (candy_version.go) as loader-result DATA; the PickCandyVersion ARBITER that consumes it stays in loaderkit (it needs kit's semver/CalVer comparison — a mechanism).

type CandyCapabilities

type CandyCapabilities = CandyCapability

--- candy sub-types ---

type CandyCapabilitiesView

type CandyCapabilitiesView struct {
	PreserveUser bool `yaml:"preserve_user,omitempty" json:"preserve_user,omitempty"`
}

#CandyCapabilitiesView — the per-candy capability surface a consumer reads off the envelope. Carries exactly the caps the validate ENGINE consumes (preserve_user today); grow it only when a relocated rule reads a further cap. Mirrors the read half of the runtime candy's `capabilities:` block (charly AggregatedCandyCaps).

type CandyCapability

type CandyCapability struct {
	PreserveUser bool `yaml:"preserve_user,omitempty" json:"preserve_user,omitempty"`

	NeedsRootAfterInit bool `yaml:"needs_root_after_init,omitempty" json:"needs_root_after_init,omitempty"`

	InitSystemHint string `yaml:"init_system_hint,omitempty" json:"init_system_hint,omitempty"`

	DataOnly bool `yaml:"data_only,omitempty" json:"data_only,omitempty"`

	OCILabels map[string]string `yaml:"oci_label,omitempty" json:"oci_label,omitempty"`
}

CandyCapabilities — image-level facts a candy contributes (aggregated at resolve time). oci_label is a genuine open string→string passthrough.

type CandyData

type CandyData struct {
	Src string `yaml:"src,omitempty" json:"src"`

	Volume string `yaml:"volume,omitempty" json:"volume"`

	Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
}

DataYAML — stage candy-dir data into a volume at build, provision at deploy.

type CandyExtract

type CandyExtract struct {
	Source string `yaml:"source,omitempty" json:"source"`

	Path string `yaml:"path,omitempty" json:"path"`

	Dest string `yaml:"dest,omitempty" json:"dest"`
}

ExtractYAML — copy a path out of another OCI image into this one. source is an image ref; path (in the source image) and dest (in this image) are absolute (validateCandyContents).

type CandyHook

type CandyHook struct {
	PostEnable string `yaml:"post_enable,omitempty" json:"post_enable,omitempty"`

	PreRemove string `yaml:"pre_remove,omitempty" json:"pre_remove,omitempty"`
}

HooksConfig — lifecycle hook scripts.

type CandyMCPProvide

type CandyMCPProvide struct {
	Name string `yaml:"name,omitempty" json:"name"`

	URL string `yaml:"url,omitempty" json:"url"`

	Transport string `yaml:"transport,omitempty" json:"transport,omitempty"`
}

MCPServerYAML — mcp_provide entry exposed to peer containers.

type CandyModel

type CandyModel struct {
	// --- identity ---
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	SourceDir string `yaml:"source_dir,omitempty" json:"source_dir,omitempty"`

	Reboot bool `yaml:"reboot,omitempty" json:"reboot,omitempty"`

	// --- host-precomputed predicates (#67 render-DRIVE move) ---
	// has_content / has_install_files are the LIVE *Candy.HasContent() / HasInstallFiles()
	// verdicts, computed HOST-SIDE (the live Candy has the env/ports/route/volumes/aliases/
	// libvirt/init fields + the fs-probe caches the envelope CandyModel cannot recompute
	// faithfully) and carried here so the specCandyAdapter matches the live *Candy
	// byte-exactly — the candy-graph composition (ExpandCandy/ResolveCandyOrder) and the
	// pixi-bound intermediate detection both gate on these. has_content does NOT count plan
	// steps, so a pure-composition candy (e.g. agent-forwarding: candy:
	// [gnupg,direnv,ssh-client], only a check plan) has has_content=false. Such a candy is
	// still admitted to the candy graph when it carries a PLAN, so that plan bakes into the
	// ai.opencharly.description label (ADE: every candy's plan is runnable acceptance); it
	// contributes nothing to the build, since has_content=false implies empty RunOps. A
	// content-free composing candy with no plan is skipped, as a pure grouping node.
	// Admission is deliberately NOT keyed to the description: ADE makes a description
	// mandatory on every candy, so that would admit every composing candy unconditionally.
	// The admission predicate is deploykit's layerEntersOrder.
	HasContent bool `yaml:"has_content,omitempty" json:"has_content,omitempty"`

	HasInstallFiles bool `yaml:"has_install_files,omitempty" json:"has_install_files,omitempty"`

	// --- plan + lowered ops ---
	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	RunOps []Op `yaml:"run_ops,omitempty" json:"run_ops,omitempty"`

	// --- services / extract / data / apk ---
	Service []CandyService `yaml:"service,omitempty" json:"service,omitempty"`

	Extract []CandyExtract `yaml:"extract,omitempty" json:"extract,omitempty"`

	Data []CandyData `yaml:"data,omitempty" json:"data,omitempty"`

	Apk []ApkPackageSpec `yaml:"apk,omitempty" json:"apk,omitempty"`

	// --- package surface (resolved) ---
	TopPackages []string `yaml:"top_packages,omitempty" json:"top_packages,omitempty"`

	// packaging — the candy's `packaging:` section (the single source of truth
	// for distro package metadata + variants). Carried on the build model so the
	// specCandyAdapter can implement CandyReader.Packaging() — the deploy-plan
	// compiler reads it to emit a LocalPkgInstallStep (PackageName = packaging.name).
	Packaging *Packaging `yaml:"packaging,omitempty" json:"packaging,omitempty"`

	FormatSections map[string]PackageSection `yaml:"format_sections,omitempty" json:"format_sections,omitempty"`

	TagSections map[string]TagPkgConfig `yaml:"tag_sections,omitempty" json:"tag_sections,omitempty"`

	// --- env / vars / route / shell ---
	Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`

	Env *EnvConfig `yaml:"env,omitempty" json:"env,omitempty"`

	Route *RouteConfig `yaml:"route,omitempty" json:"route,omitempty"`

	Shell *Shell `yaml:"shell,omitempty" json:"shell,omitempty"`

	// --- volumes / aliases ---
	Volumes []CandyVolume `yaml:"volumes,omitempty" json:"volumes,omitempty"`

	Aliases []CandyAlias `yaml:"aliases,omitempty" json:"aliases,omitempty"`

	// --- security / hooks (the OCI-label-collector surface: CollectSecurity/CollectHooks
	// read this per-candy field then apply the box-level #ResolvedBoxView.security override) ---
	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	Hook *CandyHook `yaml:"hook,omitempty" json:"hook,omitempty"`

	// --- validate read-surface (candy-local config the validate ENGINE checks) ---
	// external_builder: the reserved word of an EXTERNAL builder plugin this candy selects
	// (from the candy manifest external_builder:). validateCandyContents reads it to accept a
	// candy whose only content is an external-builder selection (deploykit EmitExternalBuilderStages).
	ExternalBuilder string `yaml:"external_builder,omitempty" json:"external_builder,omitempty"`

	// (the candy-level `libvirt:` field was removed here alongside #Candy — see
	// candy.cue's note; CollectLibvirtSnippets, its only reader, was already dead.)
	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	PortRelayPorts []int `yaml:"port_relay,omitempty" json:"port_relay,omitempty"`

	ServiceFiles []string `yaml:"service_files,omitempty" json:"service_files,omitempty"`

	EnvRequire []EnvDependency `yaml:"env_require,omitempty" json:"env_require,omitempty"`

	EnvAccept []EnvDependency `yaml:"env_accept,omitempty" json:"env_accept,omitempty"`

	SecretRequire []EnvDependency `yaml:"secret_require,omitempty" json:"secret_require,omitempty"`

	SecretAccept []EnvDependency `yaml:"secret_accept,omitempty" json:"secret_accept,omitempty"`

	MCPRequire []EnvDependency `yaml:"mcp_require,omitempty" json:"mcp_require,omitempty"`

	MCPAccept []EnvDependency `yaml:"mcp_accept,omitempty" json:"mcp_accept,omitempty"`

	// --- W9 mass-edit interface-completeness fill: the remaining fields the 42-file
	// CandyReader repoint needs, already authored on #Candy (candy.cue) but not yet
	// projected onto the build model. artifact/requires_capability/secret/port mirror
	// candy.cue's own fields+shapes exactly; capability widens the CandyView's narrow
	// (preserve_user-only) #CandyCapabilitiesView to the full authored #CandyCapability.
	Artifact []CandyArtifact `yaml:"artifact,omitempty" json:"artifact,omitempty"`

	RequiresCapability []string `yaml:"requires_capability,omitempty" json:"requires_capability,omitempty"`

	Capability *CandyCapability `yaml:"capability,omitempty" json:"capability,omitempty"`

	Secret []CandySecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	// port mirrors candy.cue's own union shape exactly (a plain int OR a "proto:port" string,
	// normalized Go-side to PortSpec — there is no reusable #PortSpec def, per candy.cue's own note).
	Port []PortSpec `yaml:"port,omitempty" json:"port,omitempty"`

	// bake_plugin: the FINAL bare-string form (post remote-sibling qualification — see
	// spec.CandyRefs, the hand-written pipeline-state type carrying the rich pre-qualification
	// form). generate.go's emitBakedPlugins reads this to COPY the plugin's pre-built provider
	// binary into every composing image.
	BakePlugin []CandyRef `yaml:"bake_plugin,omitempty" json:"bake_plugin,omitempty"`
}

#CandyModel — the serializable candy build model. Every field optional (a projection). name is the stable key. Field order mirrors the runtime *Candy accessors (deploykit.CandyModel interface).

type CandyReader

type CandyReader interface {
	GetName() string
	GetSourceDir() string
	GetVersion() string
	Vars() map[string]string
	PlanSteps() []Step
	Reboot() bool
	Apk() []ApkPackageSpec
	EnvConfig() (*EnvConfig, error)
	Service() []CandyService
	Shell() *Shell
	TopPackages() []string
	FormatSection(name string) *PackageSection
	// Packaging returns the candy's `packaging:` section (the single source of
	// truth for distro package metadata + variants). Nil when the candy declares
	// none — the deploy-plan compiler then emits no LocalPkgInstallStep for it.
	Packaging() *Packaging
	TagSection(tag string) *TagPkgConfig
	HasFile(filename string) bool

	// P8 render-delta accessors (build-mode graph/render surface). Field-backed
	// members use Get* to avoid a field/method name collision on the charly *Candy
	// implementer; the three already-methods keep their names.
	GetIncludedCandy() []CandyRefEntry // candy: composition refs (splicing)
	GetRequire() []CandyRefEntry       // require: deps (ordering + resolution)
	// GetBakePlugin (W9): bake_plugin: out-of-tree plugin candies baked into composing images
	// (generate.go emitBakedPlugins). Same bare-string-wrapped-as-CandyRefEntry shape as
	// GetRequire/GetIncludedCandy above (CandyModel.BakePlugin is the FINAL bare-string wire
	// form; the adapter re-wraps it so callers keep using .Bare() uniformly).
	GetBakePlugin() []CandyRefEntry
	HasContent() bool
	HasInstallFiles() bool // at least one install file (drives pixi-bound detection)
	PixiManifest() string
	GetHasPackageJson() bool
	GetHasCargoToml() bool
	GetExternalBuilder() string // external_builder: word (the out-of-tree builder plugin this candy selects)
	HasFormatPackages() bool
	GetRemote() bool // true if the candy came from a remote repo
	HasExtract() bool
	Extract() []CandyExtract
	HasData() bool
	Data() []CandyData
	GetHasPixiLock() bool
	GetRepoPath() string
	GetSubPathPrefix() string
	HasEnv() bool
	HasRoute() bool
	Route() (*RouteConfig, error)

	// P8 init-cluster accessors (emitInitAssembly/emitInitFragmentStages/
	// generateInitFragments). RelayPorts wraps the field-backed PortRelayPorts to
	// avoid a field/method name collision on the charly *Candy implementer.
	HasInit(initName string) bool // this candy contributes to the named init system
	ServiceFiles() []string       // file_copy-model service unit paths (globbed)
	RelayPorts() []int            // port_relay: ports (init-agnostic)

	// P8 writeCandySteps accessors.
	HasTasks() bool // the candy has any tasks: (runOps non-empty)
	RunOps() []Op   // the candy's plan lowered to build-mode run ops

	// OCI-label-collector surface (CollectSecurity/CollectHooks/layer_secrets):
	// per-candy security/hooks + the six env/secret/mcp dependency lists.
	Security() *Security
	Hooks() *CandyHook
	EnvRequire() []EnvDependency
	EnvAccept() []EnvDependency
	SecretRequire() []EnvDependency
	SecretAccept() []EnvDependency
	MCPRequire() []EnvDependency
	MCPAccept() []EnvDependency

	// W9 mass-edit interface-completeness fill: the remaining accessors the 42-file
	// repoint needs (CollectBoxAlias/CollectBoxVolume/ports.go/layer_capabilities.go/…)
	// — every one of these already exists on the charly *Candy implementer (layers.go);
	// this is a pure exposure widening, no new *Candy behavior. (Libvirt()/HasLibvirt()
	// were added here for this same fill, to serve charly/libvirt.go's
	// CollectLibvirtSnippets — that caller was dead code [box-level `libvirt:` was
	// removed in the VM hard-cutover] and was deleted, and the candy-level `libvirt:`
	// field itself was removed from the schema in the SAME cutover [candy.cue /
	// candymodel.cue, migrated by candy/plugin-migrate's stripCandyLibvirtField] — so
	// these two accessors are gone too, not merely deprecated.)
	// Consumer-set audited: HasAnyInit/HasAnyPackages/HasApk/HasService/HasTagPackages
	// were considered and DROPPED — zero external caller outside layers.go itself (they
	// back OTHER *Candy-internal composition, e.g. HasContent/HasInstallFiles, which
	// stays a host-precomputed field per the #67 predicate-carrying pattern above).
	Alias() []CandyAlias
	HasAliases() bool
	Volume() []CandyVolume
	HasVolumes() bool
	Artifact() []CandyArtifact
	Capabilities() *CandyCapability
	RequiresCapabilities() []string
	Engine() string
	EnvProvides() map[string]string
	MCPProvide() []CandyMCPProvide
	Secret() []CandySecret
	Port() ([]string, error)
	PortSpecs() []PortSpec
	HasEnvAccepts() bool
	HasEnvProvides() bool
	HasEnvRequires() bool
	HasMCPAccepts() bool
	HasMCPProvides() bool
	HasMCPRequires() bool
	HasPorts() bool
	HasSecretAccepts() bool
	HasSecretRequires() bool

	// W9 pipeline-retype fill: the candy's OWN `plugin:` declaration (the data-read half of external
	// verb/step-word recognition, plus findCommandPluginCandy) — the provider REGISTRATION itself
	// stays core-M (it touches the providerRegistry), but recognizing which capability words a
	// scanned candy declares is a plain data read the interface must expose once *Candy is gone. The
	// validate path reads exactly these three off the resolved-project envelope and hands the
	// capability strings to the host's "validate-word-sets" seam, which does the registration.
	IsPluginCandy() bool
	GetPluginSource() string
	GetPluginProviders() []string

	// W9 pipeline-retype fill: identity-view scalars multiple OCI-label-collector consumers
	// (render_baked_metadata.go's candyStatus/worst-of-status + info-parts walk) need directly —
	// widely-needed enough to earn a real interface method, unlike the FULL identity view
	// (Description/Info/IsPlugin's sibling fields + the FormatSections/TagSections/RunOps maps)
	// resolved_project_host.go alone needs via the RawCandy() escape hatch.
	GetStatus() string
	GetDescription() string

	// W9 federated-control-plane fill: the candy-declared `agent_provide:`/`terminal_profile:`
	// surfaces (56b1a6b) the OCI-label-collector (render_baked_metadata.go's per-box
	// agent/terminal aggregation) needs once *Candy is gone.
	AgentProvide() []AgentRuntimeCapability
	HasAgentProvides() bool
	TerminalProfiles() map[string]TerminalProfile
}

candy_reader.go — CandyReader, the read-only view of a runtime candy that the deploy-plan compiler (deploykit.BuildDeployPlan) and the build-render engine consume. It lives in spec (the shared contract home, alongside the CUE-generated wire envelopes) rather than sdk/deploykit so an import-clean charly file can reach it WITHOUT crossing the mechanism-kit import-purity line — matches the DocParser/ProjectWalker precedent in loader_seam.go. The concrete runtime Candy (charly core) implements it; deploykit's specCandyAdapter implements it from the CUE-generated CandyModel/CandyView wire views. Every referenced type below is already spec-native (Step/EnvConfig/EnvDependency/Op/Security/CandyHook/CandyService/Shell/…) or promoted here alongside the interface (CandyRefEntry, below) — a spec-hosted contract never needs an sdk mechanism-kit import.

type CandyRecord added in v0.2026215.1748

type CandyRecord struct {
	SchemaVersion string `yaml:"schema_version,omitempty" json:"schema_version,omitempty"`

	Candy string `yaml:"candy,omitempty" json:"candy"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	DeployedBy []string `yaml:"deployed_by,omitempty" json:"deployed_by"`

	DeployedAt string `yaml:"deployed_at,omitempty" json:"deployed_at"`

	BuilderImage string `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`

	Steps []StepRecord `yaml:"steps,omitempty" json:"steps,omitempty"`

	ReverseOps []ReverseOp `yaml:"reverse_ops,omitempty" json:"reverse_ops,omitempty"`
}

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

type CandyRef

type CandyRef = string

--- named-pattern scalars (string) ---

type CandyRefEntry

type CandyRefEntry struct {
	Raw string // original ref, e.g. "@github.com/org/repo/candy/x:v1" or bare "x"
	// Resolved is the qualified candy-map key assigned when a freshly-fetched
	// remote candy's plain-name sibling deps are qualified to
	// "<repo>/<subpathprefix><name>" (qualifyRemoteSiblingDeps). Empty for every
	// other ref, where the map key derives from Raw.
	Resolved string
}

CandyRefEntry is a single candy reference as authored in the candy manifest `require:` / `candy:` (or charly.yml `candy:`). It carries the ORIGINAL ref string — with any `@repo` prefix and `:version` suffix — as the single source of truth; the bare map-key form (.Bare()) and the pinned version (.Version()) are DERIVED on demand.

func ToCandyRefEntries

func ToCandyRefEntries(raw []string) []CandyRefEntry

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

func (CandyRefEntry) Bare

func (r CandyRefEntry) Bare() string

Bare returns the candy-map key (no @ prefix, no :version) — the form used for dependency resolution and graph keying.

func (CandyRefEntry) IsRemote

func (r CandyRefEntry) IsRemote() bool

IsRemote reports whether this is an @-prefixed remote ref.

func (CandyRefEntry) Version

func (r CandyRefEntry) Version() string

Version returns the pinned version (the ":vX" suffix), or "" for an unpinned remote ref or a local (bare-name) ref.

type CandyRefs

type CandyRefs struct {
	Require       []CandyRefEntry
	IncludedCandy []CandyRefEntry
	BakePlugin    []CandyRefEntry
}

CandyRefs carries the RICH require:/candy:/bake_plugin: refs (CandyRefEntry, with a mutable .Resolved) a freshly scanned candy declares.

SDD classification (hand-written, non-wire — precedent: ParsedProject/LoadedProject above, the original hand-written-contract types this seam file establishes): CandyRefs is same-process PIPELINE STATE crossing ONLY the compiled-in typed CandyScanner seam — it is never marshaled, because the loader plugin is bootstrap-critical and ALWAYS compiled-in (see the package doc above: "the loader must ALWAYS resolve... registered at init() before the first load"), so this seam never crosses a real wire the way an out-of-process plugin's gRPC envelope would. It exists only between ScanCandyManifest and the host's qualifyRemoteSiblingDeps (which sets .Resolved on a remote candy's plain-name sibling deps) and the FINAL bare-string conversion into CandyView.Require/.IncludedCandy (mirrors the pre-move projectCandyView's bareRefs() call, which ran AFTER qualification on the live *Candy — this type is what lets that same ordering survive the *Candy struct's departure). The FINAL bare-string form lands on CandyView.Require/ .IncludedCandy and CandyModel.BakePlugin (FinalizeCandyRefs, sdk/loaderkit).

type CandyRoute

type CandyRoute struct {
	Host string `yaml:"host,omitempty" json:"host"`

	Port int `yaml:"port,omitempty" json:"port"`
}

RouteYAML — generic service-route metadata (traefik / tunnel).

type CandyScanner

type CandyScanner interface {
	// ParseCandyManifest is the candy-MANIFEST parse the two scan methods below take as their
	// `parseManifest` seam. It relocated OUT of charly core in K-wave 2 cone R1 (A2 unit 2) into
	// sdk/loaderkit, so a plugin driving its own scan (candy/plugin-build's remote-repo fetch) can
	// parse manifests ITSELF instead of round-tripping to the host for every candy directory.
	//
	// It reaches core through this seam because charly/ may not import sdk/loaderkit (import
	// purity); core's parseCandyYAML is the thin wrapper that supplies the two values the mechanism
	// needs from the host — t, the registry-derived kind-recognition snapshot, and vocab, the build
	// vocabulary the misplaced-section shape guard consults. The clause-B buildCandy factory is NOT
	// on this path: an RDD spike over the whole 324-manifest corpus proved the pre-move
	// pn->genericNode->pn round trip through it was an identity (321 node-form manifests plus all 3
	// error paths, byte-identical).
	ParseCandyManifest(path string, t Threaded, vocab CandyVocab) (*Candy, error)
	// ProjectCandiesScanned scans or synthesizes a candy per uf.Candy entry off an ALREADY-LOADED
	// project — the local candy scan's body, relocated to sdk/loaderkit in K-wave 2 cone R1 (A2 unit
	// 3) so a plugin holding a *UnifiedFile no longer round-trips to the host to turn it into a
	// ScannedCandy map. Reaches core through this seam for the same import-purity reason
	// ParseCandyManifest does.
	ProjectCandiesScanned(uf *UnifiedFile, rootDir string, parseDoc func(path string) (*Candy, error)) (map[string]ScannedCandy, error)
	ScanCandyManifest(path, name, manifestName string, parseManifest func(path string) (*Candy, error)) (CandyModel, CandyView, CandyRefs, error)
	// ScanInlineCandy builds the two views for a candy declared INLINE in a unified charly.yml —
	// ly is already the parsed body (no manifest file, no parseManifest seam needed). sourceDir is
	// the charly.yml's own directory.
	ScanInlineCandy(name, sourceDir string, ly *Candy) (CandyModel, CandyView, CandyRefs)
	// ScanRemoteCandy scans specific candies out of a downloaded remote repository directory —
	// only the bare refs in wantRefs (each "github.com/org/repo/candy/x" form). Sets each result's
	// CandyView.Remote/.RepoPath/.SubPathPrefix and runs the remote-sibling-dep qualification
	// (QualifyRemoteSiblingDeps) before returning, mirroring the pre-move charly/layers.go
	// ScanRemoteCandy, which did the same two things (post-scan.Remote/RepoPath/SubPathPrefix
	// mutation, then qualifyRemoteSiblingDeps) on the live *Candy it had just built.
	ScanRemoteCandy(repoDir, repoPath string, wantRefs map[string]bool, parseManifest func(path string) (*Candy, error)) (map[string]ScannedCandy, error)
}

CandyScanner is the swappable CANDY-SCAN seam (W9): the loader plugin candy implements it (candy/plugin-loader, delegating to loaderkit.ScanCandyManifest), and the host resolves the registered loader provider to it and calls it once per candy directory. Typed (no wire envelope) — the compiled-in placement passes a live parseManifest callback (a Go function value, exactly like WalkSeams.Parser above) since the candy-manifest parse itself is registry-coupled (it threads the registered DocParser + the registry-derived Threaded snapshot) and so stays a HOST-injected seam rather than moving into loaderkit — only the SCAN+CONSTRUCT logic (fs-probes, the bake_plugin/package-derivation/port-normalization business logic) moves. Returns the two resolved envelope views (spec.CandyModel + spec.CandyView) DIRECTLY — the same shape sdk/deploykit.NewSpecCandyModel already consumes to build a spec.CandyReader, so core never needs a concrete Candy struct to hold the scan result.

ScanCandyManifest is named distinctly from the ESTABLISHED exported charly.ScanCandy(dir) (the whole-project scan-all-candies entry point, charly/layers.go) — a similar name on a single-candy-directory scan risks confusion once both exist side by side during the cutover.

type CandySecret

type CandySecret struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Target string `yaml:"target,omitempty" json:"target,omitempty"`

	Env string `yaml:"env,omitempty" json:"env,omitempty"`
}

SecretYAML — a candy-owned secret (target defaults to /run/secrets/<name>).

type CandyService

type CandyService struct {
	Name string `yaml:"name,omitempty" json:"name"`

	UsePackaged string `yaml:"use_packaged,omitempty" json:"use_packaged,omitempty"`

	Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`

	// distro restricts this entry to the named distros — a bare distro name
	// ("debian") or a versioned tag ("debian:13"). Empty = every distro (the
	// backward-compatible default). The service analogue of a check step's
	// exclude_distros: — it lets ONE candy carry per-distro-DIVERGENT packaged
	// units / exec daemons (the modular virtqemud.socket + virtnetworkd.socket
	// on Fedora/Arch vs the monolithic libvirtd.socket on Debian/Ubuntu, whose
	// libvirt is built without the split daemons) WITHOUT a <name>-host sibling
	// candy (CLAUDE.md "Init-system polymorphism"; R3). Filtered at render time
	// against the target distro tag chain (compileServiceSteps + generate.go).
	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`

	WorkingDirectory string `yaml:"working_directory,omitempty" json:"working_directory,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	After []string `yaml:"after,omitempty" json:"after,omitempty"`

	Before []string `yaml:"before,omitempty" json:"before,omitempty"`

	WantedBy []string `yaml:"wanted_by,omitempty" json:"wanted_by,omitempty"`

	Stdout string `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	StopTimeout string `yaml:"stop_timeout,omitempty" json:"stop_timeout,omitempty"`

	Scope string `yaml:"scope,omitempty" json:"scope,omitempty"`

	Enable bool `yaml:"enable,omitempty" json:"enable,omitempty"`

	// exec_start_pre / exec_start_post — ordered hook commands run around a CUSTOM
	// (exec:) entry's own start, rendered by the init system's service_template
	// (systemd: ExecStartPre= / ExecStartPost= lines, emitted in authored order).
	// They exist for guards belonging to the SERVICE's lifecycle rather than to the
	// install timeline: a plan step runs ONCE, at deploy, whereas a pre-start hook
	// re-asserts a host precondition on EVERY start (a restart included), and a
	// post-start hook is the only place a guard runs ALONGSIDE the daemon it
	// guards — the sole vantage from which that daemon's own socket/API is
	// reachable. Both are init-supervised, so a failure surfaces in the unit's
	// status instead of being swallowed; prefix a command with "-" for systemd's
	// ignore-failure semantics.
	// Canonical consumer: candy/k3s-server (the cgroup cpuset-delegation assertion
	// and the CRD-establishment wedge heal).
	ExecStartPre []string `yaml:"exec_start_pre,omitempty" json:"exec_start_pre,omitempty"`

	ExecStartPost []string `yaml:"exec_start_post,omitempty" json:"exec_start_post,omitempty"`

	Overrides *CandyServiceOverrides `yaml:"overrides,omitempty" json:"overrides,omitempty"`

	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

	Events string `yaml:"event,omitempty" json:"event,omitempty"`

	AutoStart *bool `yaml:"auto_start,omitempty" json:"auto_start,omitempty"`

	StartRetries int `yaml:"start_retry,omitempty" json:"start_retry,omitempty"`

	StartSecs int `yaml:"start_sec,omitempty" json:"start_sec,omitempty"`

	StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`

	ExitCode string `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`

	// ---- Portable lifecycle fields -------------------------------------------
	// Every field below is expressible by at least TWO of the three init systems.
	// That is the admission rule: a field only one init can honour belongs in
	// unit_options: instead, where it is named as that init's own.
	//
	// type — systemd Type=; supervisord approximates oneshot with startsecs=0;
	// OpenRC distinguishes forking via command_background.
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	// requires — a HARD dependency, distinct from after:'s ordering. systemd
	// Requires=; OpenRC depend(){ need … }, which its template already emits for
	// localmount. supervisord has no equivalent and ignores it.
	Requires []string `yaml:"requires,omitempty" json:"requires,omitempty"`

	// restart_sec — delay before a restart. systemd RestartSec=; OpenRC
	// --respawn-delay. Accepts "5s" or a bare 5 (Go-coerced; the Go field is string,
	// matching stop_timeout's shape).
	RestartSec string `yaml:"restart_sec,omitempty" json:"restart_sec,omitempty"`

	// watchdog_sec — liveness deadline. systemd WatchdogSec=; OpenRC
	// --healthcheck-timer. Requires type: notify on systemd to be meaningful.
	WatchdogSec string `yaml:"watchdog_sec,omitempty" json:"watchdog_sec,omitempty"`

	// unit_options — the ONE escape hatch for init-specific directives, keyed
	// init-name -> directive -> value.
	//
	// It exists so #ServiceRenderContext does not grow a field per init-specific
	// knob. That struct is a kind-AGNOSTIC envelope rendered by every init's
	// template; putting NotifyAccess, Slice, KillMode, RuntimeDirectory and a
	// hardening block into it would make it systemd's typed shape wearing a generic
	// name — the leak the boundary law names. With this map, a FOURTH init needs no
	// schema change, exactly as the openrc init entity needed no Go change.
	//
	// A value may be a scalar or a list; a list emits the directive once per element,
	// which is what systemd expects for repeatable directives such as
	// RuntimeDirectory= or ReadWritePaths=.
	UnitOptions map[string]map[string]any `yaml:"unit_options,omitempty" json:"unit_options,omitempty"`

	// wait_for — block the service's start until the paths it needs exist.
	//
	// Nine hand-rolled versions of this loop exist in the candy corpus today
	// (labwc, kde-selkies x2, chrome-cdp, wayvnc x3, swaync, waybar, waybar-labwc,
	// sway), each re-deriving the same poll with its own hardcoded timeout — the
	// sleeps and magic numbers R4 forbids. One of them is already wrong: labwc's
	// declares TIMEOUT=30 but sleeps 0.5s per iteration while counting +1, so it
	// actually gives up after 15 seconds. That is the failure mode a hand-rolled
	// loop has and a declared one cannot.
	//
	// Each init lowers this natively — systemd ExecStartPre=, supervisord a
	// rendered pre-exec ahead of the command, OpenRC start_pre() — so the wait is
	// SUPERVISED: a timeout fails the unit and surfaces in its status, instead of a
	// wrapper exiting non-zero and looking like a crash.
	WaitFor *ServiceWaitFor `yaml:"wait_for,omitempty" json:"wait_for,omitempty"`
}

ServiceEntry (spec). use_packaged XOR exec is a Go cross-field rule (mixed-entry polymorphism allows the SAME name twice: one packaged form, one exec form) — neither is required at the CUE layer.

func (*CandyService) EffectiveScope

func (e *CandyService) EffectiveScope() string

EffectiveScope returns the service scope, defaulting to "system".

func (*CandyService) IsPackaged

func (e *CandyService) IsPackaged() bool

IsPackaged reports whether the service is the packaged (use_packaged) form.

type CandyServiceOverrides

type CandyServiceOverrides struct {
	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	After []string `yaml:"after,omitempty" json:"after,omitempty"`

	Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`
}

type CandyView

type CandyView struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	Info string `yaml:"info,omitempty" json:"info,omitempty"`

	Remote bool `yaml:"remote,omitempty" json:"remote,omitempty"`

	RepoPath string `yaml:"repo_path,omitempty" json:"repo_path,omitempty"`

	// sub_path_prefix — the parent directory within the repo for sibling ref resolution
	// (e.g. "candy/"). Build-mode remote-candy COPY-source leg: RepoPath + SubPathPrefix +
	// ref reconstruct a remote candy's context path. Filled by the resolve projector; the
	// specCandyAdapter returns it so the plugin-side render reproduces remote COPY sources
	// WITHOUT the live *Candy (K3-D render move, #67 — the GetSubPathPrefix deferral).
	SubPathPrefix string `yaml:"sub_path_prefix,omitempty" json:"sub_path_prefix,omitempty"`

	IsPlugin bool `yaml:"is_plugin,omitempty" json:"is_plugin,omitempty"`

	// the candy's OWN declared plugin block (validatePluginCandy SUBJECT, task #60): the
	// `plugin.providers:` capability strings + `plugin.source:` so the validate plugin can check each
	// declared BUILTIN `<class>:<word>` is a member of ResolvedProject.ProviderCapabilities (the TARGET
	// set). Empty for a non-plugin candy. is_plugin stays the cheap presence bool for inspect/list.
	PluginProviders []string `yaml:"plugin_providers,omitempty" json:"plugin_providers,omitempty"`

	PluginSource string `yaml:"plugin_source,omitempty" json:"plugin_source,omitempty"`

	Require []CandyRef `yaml:"require,omitempty" json:"require,omitempty"`

	IncludedCandy []CandyRef `yaml:"candy,omitempty" json:"candy,omitempty"`

	EnvProvides map[string]string `yaml:"env_provide,omitempty" json:"env_provide,omitempty"`

	MCPProvide []CandyMCPProvide `yaml:"mcp_provide,omitempty" json:"mcp_provide,omitempty"`

	AgentProvide []AgentRuntimeCapability `yaml:"agent_provide,omitempty" json:"agent_provide,omitempty"`

	TerminalProfiles map[string]TerminalProfile `yaml:"terminal_profiles,omitempty" json:"terminal_profiles,omitempty"`

	Ports []int64 `yaml:"port,omitempty" json:"port,omitempty"`

	ServiceNames []string `yaml:"service_name,omitempty" json:"service_name,omitempty"`

	// per-candy list-subcommand sources: `charly box list routes|volumes|aliases|services`.
	// route/volumes/aliases carry the authored detail each subcommand prints (their presence IS
	// the RouteCandy/VolumeCandy/AliasCandy predicate). has_init + port_relay reconstruct the
	// InitCandy predicate (HasAnyInit() || PortRelayPorts>0) for `list services`.
	Route *RouteConfig `yaml:"route,omitempty" json:"route,omitempty"`

	Volumes []CandyVolume `yaml:"volumes,omitempty" json:"volumes,omitempty"`

	Aliases []CandyAlias `yaml:"aliases,omitempty" json:"aliases,omitempty"`

	HasInit bool `yaml:"has_init,omitempty" json:"has_init,omitempty"`

	// init_systems — the PER-INIT-SYSTEM trigger map (W9 finding: HasInit alone is one AGGREGATE
	// "any init" bool, which cannot answer "does this candy trigger init system Y" — the question
	// deploykit's HasInit(initName) / sdk/deploykit's EmitInitFragmentStages actually ask). Mirrors
	// the live *Candy.InitSystems field exactly; populated by the SAME cross-candy host-completion
	// pass PopulateCandyInitSystem runs (once, after every candy is scanned) — see
	// sdk/deploykit.specCandyAdapter.HasInit, which reads this field directly.
	InitSystems map[string]bool `yaml:"init_systems,omitempty" json:"init_systems,omitempty"`

	PortRelayPorts []int `yaml:"port_relay,omitempty" json:"port_relay,omitempty"`

	// capabilities — the per-candy capability surface the validate ENGINE reads (task #60,
	// ruling a). The projector fills it from the candy's `capabilities:` block; the validate
	// plugin re-derives it via boxPreserveUser over a box's candy order (a boolean OR of
	// preserve_user, order-independent) + the PreserveUser rule (the validatePackagedServices
	// box-arm). An aggregate over RESOLVED models belongs on the
	// envelope, not a host route-B diagnostic — a lean envelope is not a virtue when it keeps
	// an R-rule in core.
	Capabilities *CandyCapabilitiesView `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`
}

#CandyView — the resolved candy GRAPH node a consumer reads: identity + dep-graph + provides + ports/services (the exported surface of the runtime Candy). The Has* filesystem probes, the unexported package/service sections, and the *CandyPluginDecl stay host — this is NOT the candy BUILD model (plan-steps / package-format sections), which is the CandyModel / S-CM concern (K3-D), distinct by design. require / candy pin to the bare map-key ref form (#CandyRef is @go(-), so a list of it generates []string). name is the stable key.

type CandyVocab added in v0.2026218.138

type CandyVocab struct {
	// FormatNames is the union of every distro's declared package formats (rpm/deb/pac/aur/…),
	// inherited chains resolved.
	FormatNames map[string]bool
	// DistroNames is every distro name declared in the build vocabulary.
	DistroNames map[string]bool
}

candy_vocab.go — the build VOCABULARY the candy-manifest shape guard consults (K-wave 2 cone R1, A2 unit 2). Relocated from charly/layers.go, where it was the pair of process-global caches candyYAMLFormatNames / candyYAMLDistroNames plus looksLikeDistroOrFormatKey.

It became a VALUE rather than staying a global because the parse mechanism it feeds moved to sdk/loaderkit, where both charly core (via the CandyScanner seam) and candy/plugin-build (directly) call it. A compiled-in plugin shares the host's process, so package globals would have APPEARED to work while silently coupling two modules through mutable state — and would fail open for an out-of-process placement, where the plugin's own copy is never registered. Passing the value keeps the mechanism honest in both placements.

The vocabulary is NOT hardcoded in Go: it is DERIVED at load time from the embedded build vocabulary (plus any project override) — the `distro:` section (the DistroConfig). Adding a new distro or package format is therefore purely a vocabulary edit, with no code change.

These sets are consumed ONLY by the candy-manifest shape guard, to recognize a package-format or per-distro section mistakenly placed at the candy root. The FORWARD package parser (loaderkit.derivePackageSections) needs no vocabulary at all — it routes every `distro:` sub-key structurally and lets the cascade resolver match on the image's real Distro/Pkg.

func NewCandyVocab added in v0.2026218.138

func NewCandyVocab(dc *DistroConfig) CandyVocab

NewCandyVocab derives the distro/format vocabulary from a DistroConfig. A nil config yields the zero vocabulary, under which the shape guard fails OPEN (no false positives) — byte-identical to the pre-move RegisterBuildVocabulary(nil) contract, which cleared the caches for the same reason.

func (CandyVocab) LooksLikeDistroOrFormatKey added in v0.2026218.138

func (v CandyVocab) LooksLikeDistroOrFormatKey(key string) bool

LooksLikeDistroOrFormatKey reports whether a candy-manifest top-level key is a package-format family name (pac/deb/rpm/aur) or a per-distro tag section (`debian`, `debian:13`, `debian,ubuntu`) — shapes that nest under the `distro:` map, never at the candy root. Returns false for an unpopulated vocabulary (no false positives), leaving the explicit removed-field cases to fire.

type CandyVolume

type CandyVolume struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Path string `yaml:"path,omitempty" json:"path"`
}

VolumeYAML — name is lowercase-hyphen (validateVolume); path may be home-relative (~/…) or absolute, so not anchored to /.

type CandyYAML

type CandyYAML = Candy

--- top-level entity types ---

type CapabilityInitDef

type CapabilityInitDef struct {
	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	FallbackEntrypoint []string `yaml:"fallback_entrypoint,omitempty" json:"fallback_entrypoint,omitempty"`

	ManagementTool string `yaml:"management_tool,omitempty" json:"management_tool,omitempty"`

	ManagementCommands map[string]string `yaml:"management_commands,omitempty" json:"management_commands,omitempty"`
}

#CapabilityInitDef — the deploy-relevant subset of an InitDef (ai.opencharly.init_def).

type CapabilityService

type CapabilityService struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Scope string `yaml:"scope,omitempty" json:"scope,omitempty"`

	Enable bool `yaml:"enable,omitempty" json:"enable,omitempty"`

	UsePackaged string `yaml:"use_packaged,omitempty" json:"use_packaged,omitempty"`

	Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`

	WorkingDirectory string `yaml:"working_directory,omitempty" json:"working_directory,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	After []string `yaml:"after,omitempty" json:"after,omitempty"`

	Before []string `yaml:"before,omitempty" json:"before,omitempty"`

	Stdout string `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	StopTimeout string `yaml:"stop_timeout,omitempty" json:"stop_timeout,omitempty"`

	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

	Events string `yaml:"events,omitempty" json:"events,omitempty"`

	AutoStart *bool `yaml:"auto_start,omitempty" json:"auto_start,omitempty"`

	StartRetries int `yaml:"start_retries,omitempty" json:"start_retries,omitempty"`

	StartSec int `yaml:"start_sec,omitempty" json:"start_sec,omitempty"`

	StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`

	ExitCode string `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`

	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	Candy string `yaml:"candy,omitempty" json:"candy,omitempty"`
}

#CapabilityService — the full structured service spec baked into ai.opencharly.service. name required; all else omitempty. Mirrors the hand struct byte-for-byte.

type Check

type Check Deploy

#Check — a kind:check bed. Structurally IDENTICAL to #Deploy (same FleetNode Go struct), so it is a plain reference (R3 — no field duplication; stays CLOSED because #Deploy is closed). The bed-mode invariants the former `& (A|B|C)` disjunction expressed — disposable required + bed-legal target ∈ {pod,vm,local, android} for the deterministic/ephemeral modes, the iterate AI-benchmark mode, and the ephemeral⇒disposable promotion — are enforced in GO at load time (loaderkit.ValidateCheckBeds + the ephemeral validator beside it), which is the SINGLE source of truth for the actual fleet-form beds (a node-form check bed is a `fleet:` node validated via #DeployValue=#Deploy, so the disjunction was only ever applied to the legacy root-shape `check:` collection). Relaxing it to the alias removes that divergent parallel spec and lets gengotypes emit a real Check struct instead of an empty `struct{}`.

type CheckBedGpuPrereqReply added in v0.2026217.301

type CheckBedGpuPrereqReply struct {
	Missing bool `yaml:"missing,omitempty" json:"missing,omitempty"`

	Token string `yaml:"token,omitempty" json:"token,omitempty"`

	Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty"`
}

type CheckBedGpuPrereqRequest added in v0.2026217.301

type CheckBedGpuPrereqRequest struct {
	Tokens []string `yaml:"tokens,omitempty" json:"tokens,omitempty"`
}

#CheckBedRequest DIED (#55 W3 B2-full): the "check-bed" op-discriminated HostBuild seam it carried is gone. The compiled-in plugin-check now self-orchestrates the whole bed session itself — flock via spec/lock, the repo-override via spec/proc, the arbiter lease via a direct InvokeProvider(verb,"arbiter") call (the vm_arbiter_shim precedent) — exactly the K5 death this type's own header already predicted. See candy/plugin-check/bed_session.go.

#CheckBedGpuPrereqRequest/#CheckBedGpuPrereqReply is the ONE narrow seam that SURVIVES: GPU host-DETECTION (gpu_allocate.go's bedGPUPrereqMissing, DetectVFIO) is the project's explicitly operator-dropped exception (no hardware to verify against; fenced from every K-wave cutover, including this one — see gpu_shim.go's own header). Threads just the claimant's resource tokens out and the GPU-unsatisfiable verdict back, so the fenced core logic stays completely unchanged.

type CheckBedMember

type CheckBedMember struct {
	Key string `yaml:"key,omitempty" json:"key"`

	IsVM bool `yaml:"is_vm,omitempty" json:"is_vm,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	From string `yaml:"from,omitempty" json:"from,omitempty"`
}

#CheckBedMember — one sibling member's build coordinates (the group-bed member build loop).

type CheckBedPrereqSkip

type CheckBedPrereqSkip struct {
	Token string `yaml:"token,omitempty" json:"token"`

	Vendor string `yaml:"vendor,omitempty" json:"vendor"`

	Reason string `yaml:"reason,omitempty" json:"reason"`
}

#CheckBedPrereqSkip — a bed the host skips for an absent HOST prerequisite (a GPU resource whose vendor has no matching card): a clean SKIP (exit 3), not a failure.

type CheckBedReply

type CheckBedReply struct {
	Calver string `yaml:"calver,omitempty" json:"calver,omitempty"`

	LogDir string `yaml:"log_dir,omitempty" json:"log_dir,omitempty"`

	PrereqSkip *CheckBedPrereqSkip `yaml:"prereq_skip,omitempty" json:"prereq_skip,omitempty"`

	// BedDescriptor — the substrate classification + refs the plugin drives from.
	IsVM bool `yaml:"is_vm,omitempty" json:"is_vm,omitempty"`

	IsLocal bool `yaml:"is_local,omitempty" json:"is_local,omitempty"`

	IsGroup bool `yaml:"is_group,omitempty" json:"is_group,omitempty"`

	IsExternal bool `yaml:"is_external,omitempty" json:"is_external,omitempty"`

	// node_json is the bed ROOT FleetNode (spec.Deploy) serialized — including its nested
	// Members peer map (each member's full FleetNode, with stamped Descent) — so the plugin
	// bed runner can call deploykit.PersistBedDeployOverrides PLUGIN-SIDE for the bed root AND
	// each member (#55 coneC-dsh β1 — the bed-root + member persist relocate off the host seam;
	// the host-side persistBedDeployOverrides wrapper + its deploykit import shed). The plugin
	// supplies its own loader-threaded marshalNode + reader (deployMarshalNode/deployConfigReader
	// pattern); externalInPlace for the root is IsExternal above, for a member it is derivable from
	// the member's stamped Descent (Venue parent/none → in-place). Empty for a VM root (the host
	// seam's !isVM guard — a VM bed runs no `charly config`, so no root persist).
	NodeJSON RawBody `yaml:"node_json,omitempty" json:"node_json,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	HasAddCandy bool `yaml:"has_add_candy,omitempty" json:"has_add_candy,omitempty"`

	// bed) — bed_run.go skips --tag at the config/start steps for such a bed: the FRESH artifact to
	// verify is the overlay fleet-add just built + persisted (resolved via plugin-deploy-pod's
	// resolveDeployRefLocal resolved_image overlay preference), not the base
	// image's own --tag build ref.
	VMTemplate string `yaml:"vm_template,omitempty" json:"vm_template,omitempty"`

	BedDomain string `yaml:"bed_domain,omitempty" json:"bed_domain,omitempty"`

	ImageTag string `yaml:"image_tag,omitempty" json:"image_tag,omitempty"`

	LocalRef string `yaml:"local_ref,omitempty" json:"local_ref,omitempty"`

	VMDomains []string `yaml:"vm_domains,omitempty" json:"vm_domains,omitempty"`

	CheckLiveRefs []string `yaml:"check_live_refs,omitempty" json:"check_live_refs,omitempty"`

	ChildKeys []string `yaml:"child_keys,omitempty" json:"child_keys,omitempty"`

	// local_child_keys is the HOST-ROOTED (kind:local) subset of child_keys, in the same order. A VM
	// root deploys ONLY these host-side (mirroring the core deployNestedLocalChildren): a VM's
	// nested CONTAINER children are deployed in-guest by plugin-deploy-vm's PostApply, so a host-side
	// re-deploy would be wrong. The pod path uses child_keys (all); the vm path uses local_child_keys.
	LocalChildKeys []string `yaml:"local_child_keys,omitempty" json:"local_child_keys,omitempty"`

	// members carries each sibling member's build coordinates so a GROUP bed's plugin can drive the
	// per-member image build loop (`charly vm build <from>` / `charly box build <image>` + check box)
	// BEFORE the host `members-up` op deploys them (bringUpMembers assumes pre-built images).
	Members []CheckBedMember `yaml:"members,omitempty" json:"members,omitempty"`

	RunBuild bool `yaml:"run_build,omitempty" json:"run_build,omitempty"`

	RunRuntime bool `yaml:"run_runtime,omitempty" json:"run_runtime,omitempty"`

	RunAgent bool `yaml:"run_agent,omitempty" json:"run_agent,omitempty"`
}

#CheckBedReply is now a plain plugin-internal DESCRIPTOR VALUE TYPE (#55 W3 B2-full), never a wire reply — candy/plugin-check/bed_session.go constructs it directly (no HostBuild round-trip; the type stays CUE-sourced per SDD, since it is still a useful named shape the plugin builds and consumes internally). PrereqSkip set ⇒ the bed is a clean SKIP (exit 3): the plugin writes the prereq-skip summary + returns CheckSkippedError, running NO other setup step (not even teardown — nothing was acquired on the skip path).

type CheckContext added in v0.2026213.1726

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

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

type CheckDrainEndpointCleanupsRequest added in v0.2026217.301

type CheckDrainEndpointCleanupsRequest struct {
}

#CheckDrainEndpointCleanupsRequest signals plugin-check to close every forward its #CheckEndpointResolveRequest handler opened since the last drain (LIFO) — the plugin-side twin of the former core-side hostVerbResolver.runEndpointCleanups (#55 W3 B7). Carries no fields: the plugin's pending-cleanup list IS the state, reset+drained per single-verb Invoke — the SAME sequential per-Invoke lifecycle guarantee the former core-side h.endpointCleanups = nil / defer h.runEndpointCleanups() bracket relied on, now owned by the plugin instead.

type CheckEndpointResolveReply added in v0.2026217.301

type CheckEndpointResolveReply struct {
	Addr string `yaml:"addr,omitempty" json:"addr,omitempty"`
}

type CheckEndpointResolveRequest added in v0.2026217.301

type CheckEndpointResolveRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode"`

	Port int `yaml:"port,omitempty" json:"port"`
}

#CheckEndpointResolveRequest/#CheckEndpointResolveReply — the resolution BODY behind the fixed CheckContext.ResolveEndpoint reverse-RPC every out-of-process live-container verb (cdp/wl/vnc/dbus/mcp) dials back into (#55 W3 B7). The reverse-RPC SERVICE surface itself stays core (charly/check_endpoint_resolve.go's hostVerbResolver wraps the core-private verb-dispatch registry no out-of-process caller can bypass) — only the RESOLUTION WORK relocates, compiled-in-REQUIRED placement class (bed_session.go's precedent, #55 W3 B2-full): the venue-classify leg it calls was ALREADY plugin-native (#CheckVenueResolveRequest above), and the downstream resolution (spec/checkhost's EndpointForVenue) has zero core-private dependency of its own. Any ssh -L forward it opens is tracked in the plugin's OWN per-process pending-cleanup state, never on the wire (a live cleanup closure cannot cross ANY Invoke — compiled-in or not, the wire is JSON bytes only) — see #CheckDrainEndpointCleanupsRequest for the close-it-now signal.

type CheckEnv

type CheckEnv struct {
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"`

	Distros []string `yaml:"distros,omitempty" json:"distros,omitempty"`

	Venue string `yaml:"venue,omitempty" json:"venue,omitempty"`

	VenueKind string `yaml:"venue_kind,omitempty" json:"venue_kind,omitempty"`

	DialTimeoutNs int64 `yaml:"dial_timeout_ns,omitempty" json:"dial_timeout_ns,omitempty"`
}

#CheckEnv is the SINGLE-SOURCED scalar snapshot of a check verb's invocation context (K1-unblock W3 Unit B) — the ONE #CheckEnv def now generating the struct all THREE of its consumers share (a hand-written mirror per consumer is the exact "wire type not CUE-generated" violation SDD forbids in fresh code): (1) charly/provider_checkenv.go's host-side CheckEnv, filled by snapshotCheckEnv from a live *kit.Runner and threaded to an out-of-process verb's Invoke envelope; (2) sdk's out-of-process verb-serve decode (sdk/checkverb.go), which reconstructs a kit.CheckContext's scalar legs from this exact snapshot; (3) candy/plugin-check's InvokeProvider-backed VerbResolver (verb_resolver.go), which marshals this same shape when asking the host to dispatch a verb on its behalf. A 4th consumer (charly/plugin_dispatch_reverse.go's InvokeProvider host handler) DECODES it host-side to construct a detached kit.CheckContext for a CheckVerbProvider target — the SAME snapshot, not a second shape.

Every field is optional (a caller fills only what it has — an in-proc/live snapshot has a live Runner to read from; a box-mode run has no ContainerName/Venue; a detached construction may lack DialTimeoutNs). container_name/venue are HOST-COMPUTED-ONLY fields (never authored, carried for the out-of-process appium/vm-target verbs that need charly's naming convention without re-deriving it) — present on the wire regardless of which of the three marshal sites populates them, since all three now share this one shape.

type CheckExecutor added in v0.2026213.1726

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

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

type CheckGraphicsEndpoint added in v0.2026213.1726

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

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

type CheckHTTPRequest added in v0.2026213.1726

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

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

type CheckHTTPResponse added in v0.2026213.1726

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

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

type CheckImageLabelResolveReply added in v0.2026217.301

type CheckImageLabelResolveReply struct {
	Value string `yaml:"value,omitempty" json:"value,omitempty"`
}

type CheckImageLabelResolveRequest added in v0.2026217.301

type CheckImageLabelResolveRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode"`

	Label string `yaml:"label,omitempty" json:"label"`
}

#CheckImageLabelResolveRequest/#CheckImageLabelResolveReply — the resolution BODY behind the fixed CheckContext.ResolveImageLabel reverse-RPC (#55 W3 B7), the sibling of #CheckEndpointResolveRequest above (same placement class; no live resource to track — a raw OCI label read is a pure podman-inspect computation).

type CheckLoadPluginsReply

type CheckLoadPluginsReply struct {
}

#CheckLoadPluginsReply is empty on success — connect failures are best-effort WARNINGS on the host (mirroring resolveCheckRunnerContext's existing behavior: an unresolvable plugin fails loudly later, at actual verb dispatch, never here).

type CheckLoadPluginsRequest

type CheckLoadPluginsRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
}

#CheckLoadPluginsRequest asks the host to connect the out-of-process plugin candies a check plan's verb words reference (K1-unblock wave — the "live" check-run arm). Verb dispatch itself crosses the wire generically via InvokeProvider (S1 — command:check's pluginVerbResolver), but InvokeProvider only resolves an ALREADY-CONNECTED provider (or a compiled-in one); connecting an out-of-process candy is the plugin-loading M-mechanism (the kernel/plugin boundary law's clause M — plugin discovery/loading/connect stays core), so this seam is the entry point a plugin calls BEFORE dispatching a plan whose verbs may need an out-of-process candy connected. The host runs the UNCHANGED core engine (LoadConfig + resolveCheckRunnerContext: ScanAllCandyWithConfigOpts + collectReferencedPluginWords + loadProjectPlugins) as a pure SIDE EFFECT on its own providerRegistry — every subsequent InvokeProvider call in this same `charly check run` invocation then resolves. Class-generic action noun "check-load-plugins" (F11 — never a substrate/provider word).

type CheckResult

type CheckResult struct {
	Op *Op `yaml:"op,omitempty" json:"op"`

	Verb string `yaml:"verb,omitempty" json:"verb"`

	Status Status `yaml:"status,omitempty" json:"status"`

	Message string `yaml:"message,omitempty" json:"message"`

	Elapsed time.Duration `yaml:"elapsed,omitempty" json:"elapsed"`

	Attempts int `yaml:"attempts,omitempty" json:"attempts,omitempty"`

	TotalElapsed time.Duration `yaml:"total_elapsed,omitempty" json:"total_elapsed,omitempty"`

	CapturedValue string `yaml:"captured_value,omitempty" json:"captured_value,omitempty"`
}

CUE schema for the check-engine's per-step VERDICT envelope (FLOOR-SLIM Unit 4). NOT an authoring kind (never in #Node/#Op) — a pure generated wire/render struct, single-sourced here so `task cue:gen` produces the Go struct charly core's registry-coupled floor files (provider.go/provider_verb.go/verb_builtins.go/unified_targets.go/provider_checkenv.go) reference directly (spec.CheckResult), with zero new sdk/kit import.

#CheckResult covers every field EXCEPT the engine-internal `DeadlineExceeded` retry signal — the ONE spike-proven exception (P12, cited at sdk/kit/checkrun_seam.go and sdk/schema/seam.cue): `DeadlineExceeded bool json:"-"` has no gengotypes construct (a field that exists in memory but is excluded from marshaling). sdk/kit.CheckResult embeds this generated type and adds ONLY that one hand-written field back.

FLOOR-SLIM deliberately renames the wire keys to snake_case (Op→op, Verb→verb, Status→status, Message→message, Elapsed→elapsed) — the former hand-written type carried NO json tag on these fields, so encoding/json defaulted to the bare, inconsistent PascalCase Go field name. This is a documented, deliberate breaking wire-format fix for `--format json`/TAP consumers of `charly check box/live/run`, not an accident: it brings CheckResult in line with every other CUE-sourced wire type's snake_case convention. Every field that ALWAYS serialized before (op/verb/status/message/elapsed) stays REQUIRED (`!`) here so gengotypes omits `omitempty` — an omitempty regression would silently drop zero-valued fields from output, a SEPARATE wire change the rename must not introduce. Every field that already carried `,omitempty` (attempts/total_elapsed/captured_value) stays optional (`?`).

Status is carried as a plain int (@go(Status,type=Status) — Status is the check-engine's pass/fail/skip enum, HAND-WRITTEN in spec/status_result.go: gengotypes has no construct for an iota-based enum + String() method, so CUE owns the wire VALUE SET (an int) and Go owns the formatting behavior (String()), mirroring the #SubstrateKind split (status.cue) — there the enum is string-backed and suppressed via @go(-); here it is int-backed and referenced directly since there is no separate disjunction def to suppress.

Elapsed / TotalElapsed carry a nanosecond count wire-typed as time.Duration (@go(Elapsed,type=time.Duration)) — the RDD spike (T-P12, cited in /charly-internals:go) proved a custom-scalar @go(,type=…) override generates faithfully.

type CheckRunMode added in v0.2026213.1726

type CheckRunMode int

CheckRunMode is the mode a check runs under (charly's RunMode). It never crosses the wire — spec.CheckEnv carries the "box"/"live" string form — so it is a plain Go int.

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

func (CheckRunMode) String added in v0.2026213.1726

func (m CheckRunMode) String() string

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

type CheckRunReply added in v0.2026213.1726

type CheckRunReply struct {
	Steps []StepResult `yaml:"steps,omitempty" json:"steps,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	NoSteps bool `yaml:"no_steps,omitempty" json:"no_steps,omitempty"`

	Header string `yaml:"header,omitempty" json:"header,omitempty"`

	Passthrough *StepPass `yaml:"passthrough,omitempty" json:"passthrough,omitempty"`

	Score *CheckRunResults `yaml:"score,omitempty" json:"score,omitempty"`
}

#CheckRunReply is the plugin-resolved result of a check-run. Steps is the per-step verdict list the plugin formats (FormatStepResults*) and tallies into an exit code. Image is the resolved image ref for the "Image: <ref>" header line. NoSteps signals the image declared no plan (the plugin prints "No plan steps defined for this image." and exits 0) — distinct from an empty Steps that ran zero scored steps. Header is the pre-formatted, kind-specific banner line the plugin builds (container name, ssh user/host/port, member count) — the plugin prints it, then the formatted Steps. Passthrough carries the one non-plan-run live path — a nested pod-in-VM leaf whose check is delegated to the guest over SSH, forwarded verbatim; nil for every plan-run mode. Score is the "score"-mode reply (the AI-harness SCORING result, #CheckRunResults), nil for the box/live/feature plan-run modes that carry their verdicts in Steps. CUE-sourced (SDD): plain carrier, no json:"-".

type CheckRunRequest

type CheckRunRequest struct {
	Mode string `yaml:"mode,omitempty" json:"mode"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	Section string `yaml:"section,omitempty" json:"section,omitempty"`

	Filter []string `yaml:"filter,omitempty" json:"filter,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Strict bool `yaml:"strict,omitempty" json:"strict,omitempty"`

	Agent string `yaml:"agent,omitempty" json:"agent,omitempty"`

	Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`

	NoAgent bool `yaml:"no_agent,omitempty" json:"no_agent,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	// --- snapshot-anchored check-run mode (§5.3.1) ---
	// anchor: the snapshot name to revert before the checks (the golden disk).
	// keep_venue: keep the VM between runs (batch loop). variant: the VM-config
	// variant name to boot over the shared golden disk. vars: per-run variable
	// passthrough (e.g. pr=9345).
	Anchor string `yaml:"anchor,omitempty" json:"anchor,omitempty"`

	KeepVenue bool `yaml:"keep_venue,omitempty" json:"keep_venue,omitempty"`

	Variant string `yaml:"variant,omitempty" json:"variant,omitempty"`

	Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`
}

#CheckRunRequest is the check-run dispatch envelope (P12). command:check (candy/plugin-check) owns the `charly check` CLI + output formatting AND runs every mode itself (hostCheckRunCtx's per-mode bodies — the former "check-run" HostBuild kind is DELETED, K-wave 2 cone R4): the venue→executor construction + OCI-label plan extraction are plugin-side (loaderkit / deploykit), while the plan-walk's verb dispatch resolves through the provider registry (in-proc, or the out-of-process check-context reverse channel for the live-container verbs). The action noun "check-run" is class-generic (F11).

Mode selects the run shape (discriminated union): "box" — a pure-box run against a disposable container built from Image (RunModeBox, build-scope steps only, the CheckBoxCmd engine); "live" — a full-stack run against a running deployment resolved by Name (the plugin classifies vm/pod/local/group via its own venue.go), applying the Instance/Section/Filter selectors; "feature-box" / "feature-live" — the ADE acceptance run (SkipDeterministicRun) over Image (build scope) or the live deployment Name (deploy scope, the plugin-side agent grader wiring, gated by NoAgent/Agent/Timeout), scoped by Tag/Strict. Dir is the project dir (empty → the plugin uses its own cwd), matching LoadUnified(dir). `format` is deliberately NOT a field — the plugin formats the returned Steps itself. run-bed + iterate are NOT seam modes: the plugin drives them over HostBuild("cli").

The REPLY is CUE-sourced: #CheckRunReply / #StepResult / #StepPass (checkresult.cue) — generated to spec.CheckRunReply / spec.StepResult / spec.StepPass, byte-identical JSON to the former hand-written kit types. sdk/kit aliases each (kit.CheckRunReply = spec.CheckRunReply etc., sdk/kit/checkrun_seam.go) so the plugin reuses the kit formatters (FormatStepResults*) with byte-parity across every --format. A live `cue exp gengotypes` spike (P12) proved kit.CheckResult AS A WHOLE is genuinely inexpressible in CUE — its engine-internal `DeadlineExceeded bool json:"-"` field has no gengotypes construct — but confirmed the REST of the type (Op/Verb/Status/Message/Elapsed/Attempts/TotalElapsed/CapturedValue) generates faithfully. FLOOR-SLIM Unit 4 acted on that finding: #CheckResult (checkresult.cue) is the CUE-sourced base (→ spec.CheckResult), and kit.CheckResult is now `struct { spec.CheckResult; DeadlineExceeded bool json:"-" }` — an EMBEDDING wrapper, not a hand-duplicated type. The SDD sweep in THIS cutover extended CUE-sourcing to the reply envelope too: StepResult / CheckRunReply / StepPass are plain carriers (no json:"-"), so gengotypes expresses them faithfully — the `optional=nillable` marker emits the Passthrough / Score pointers. The exception the wire mandate's spike-proven path authorizes is now narrowed to EXACTLY the one kit.CheckResult field that forced it, not the whole type nor the reply envelope.

P12 Wave-2: the "score" mode adds Plan — a substituted, nonce-carrying scoring plan candy/plugin-check's pluginRunCheckLive walks (NOT the OCI-baked plan the "live" mode extracts; walked plugin-side directly since K1-unblock wave arm 3, no host round-trip). Its per-step scoring verdicts ride the kit.CheckRunReply.Score field (a *CheckRunResults, below).

type CheckRunResults

type CheckRunResults struct {
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Step []StepScore `yaml:"step,omitempty" json:"step,omitempty"`

	Summary ScoreSummary `yaml:"summary,omitempty" json:"summary"`
}

#CheckRunResults / #StepScore / #ScoreSummary — the AI-harness SCORING result model (originally P12 Wave-2; the mode's walk moved plugin-side in K1-unblock wave arm 3). pluginRunCheckLive returns a *CheckRunResults (the scored check:/agent-check: verdicts, keyed by step id for plateau tracking); it doubles as the `charly check box --format yaml` payload the harness scorer parses (ParseCharlyTestOutput). These are plain structs — the gengotypes workhorse — CUE-sourced so the "score"-mode reply's Score field and the plugin scorer that produces AND consumes it import ONE definition (SDD; no alias). Every field mirrors the former hand-written Go tag set: required (!) fields carry no json-omitempty (json wire byte-identical for the seam reply); optional (?) fields carry it. The retag pass adds ,omitempty to every YAML tag uniformly — inert here since ID/Status are always set and a zero Summary block only elides on an empty (0-step) result ParseCharlyTestOutput tolerates.

type CheckVenueResolveReply

type CheckVenueResolveReply struct {
	Descriptor VenueDescriptor `yaml:"descriptor,omitempty" json:"descriptor"`

	Kind string `yaml:"kind,omitempty" json:"kind"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	VMName string `yaml:"vm_name,omitempty" json:"vm_name,omitempty"`

	Nested bool `yaml:"nested,omitempty" json:"nested,omitempty"`
}

#CheckVenueResolveReply is the wire-safe projection of the plugin-side CheckVenue: the generic #VenueDescriptor (the host re-materializes it into a live DeployExecutor — a live executor never crosses the wire) plus the scalar venue facts the host legs consume (kind for the vm-vs-not graphics branch + the IsContainer var-resolution gate; engine/name for container port mapping + image-label reads; vm_name for the VM graphics leg; nested marks a genuinely multi-hop target the single-hop descriptor degrades to zero for, so the host rebuilds its chain via ResolveDeployChain).

type CheckVenueResolveRequest

type CheckVenueResolveRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#CheckVenueResolveRequest asks plugin-check to CLASSIFY a check target's venue — the kind-decode (checkVmTarget/checkLocalTarget branch on the stamped venue trait) that is CHECK CAPABILITY LOGIC, not kernel fabric (#118 boundary-law self-test: a floor file switching on a kind is a leaked R-item). The host's floor reverse-legs (endpoint / graphics / gRPC exec-attach) call this INSTEAD of an in-core classifier duplicate, then re-materialize the returned generic #VenueDescriptor via kit.VenueFromDescriptor (single-hop) — or, for a nested target the flat descriptor cannot express, rebuild the N-hop chain host-side via the kind-blind deploykit.ResolveDeployChain. plugin-check reaches the merged deploy tree via its OWN InvokeProvider("build","project", OpResolve) peer-dispatch (the former HostBuild("resolved-project") seam is DELETED), so this seam carries only name+instance. Class-generic action noun (F11).

type CheckVerbProvider added in v0.2026213.1726

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

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

type CheckVerbResult added in v0.2026213.1726

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

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

type CliReply

type CliReply struct {
	Stdout string `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	ExitCode int `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

#CliReply is the "cli" host-builder reply: captured stdout (Capture=true), the exit code, and an error string on a non-zero exit that was not BestEffort-swallowed.

type CliRequest

type CliRequest struct {
	Argv []string `yaml:"argv,omitempty" json:"argv"`

	Capture bool `yaml:"capture,omitempty" json:"capture,omitempty"`

	Combined bool `yaml:"combined,omitempty" json:"combined,omitempty"`

	BestEffort bool `yaml:"best_effort,omitempty" json:"best_effort,omitempty"`
}

#CliRequest is the "cli" host-builder envelope (M4): a lifecycle plugin asks the HOST to run a `charly <argv>` subcommand.

type CloudInitRuntimeParams added in v0.2026215.1748

type CloudInitRuntimeParams struct {
	// SSHPublicKey is the OpenSSH authorized_keys-format public key
	// line (e.g. "ssh-ed25519 AAAA..."). Empty when key injection is
	// disabled or when VmSsh.KeySource == "none".
	SSHPublicKey string

	// InstanceID is the stable UUIDv4 cloud-init instance-id.
	// Pinned at first VM create and persisted in VmDeployState.
	InstanceID string

	// Hostname for the guest. Defaults to the VM name when empty.
	Hostname string

	// InjectKeyViaCloudInit is the resolved D13 key_injection.cloud_init
	// channel state. When false the renderer emits no
	// ssh_authorized_keys entries even if SSHPublicKey is populated.
	InjectKeyViaCloudInit bool
}

CloudInitRuntimeParams carries the runtime-resolved state needed to render cloud-init user-data: the SSH public key to inject, the instance-id (stable UUIDv4 persisted in VmDeployState), the hostname, and whether cloud-init should inject the SSH key at all (computed from D13 auto-defaults + explicit VmKeyInjection overrides).

type CollectedAlias

type CollectedAlias struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Command string `yaml:"command,omitempty" json:"command"`
}

#CollectedAlias — a command alias baked into ai.opencharly.alias (name+command required).

type Config

type Config struct {
	Defaults BoxConfig `yaml:"defaults" json:"defaults"`
	Box      BoxMap    `yaml:"box" json:"box"`
	// Local carries kind:local templates so remote-ref collection + validation walk their candy:
	// lists symmetrically with box candy lists (kind:local templates compose remote @-ref
	// candies too). Populated from UnifiedFile.Local by UnifiedFile.ProjectConfig() (sdk/loaderkit).
	Local map[string]json.RawMessage `yaml:"local,omitempty" json:"local,omitempty"`
	// Sidecar carries the project's sidecar-template library as OPAQUE bodies (the raw
	// PluginKinds["sidecar"] map). The kernel never reads their fields — candy/plugin-sidecar's
	// OpResolve owns all sidecar business logic.
	Sidecar map[string]json.RawMessage `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
	// Skills carries the project's skill-kind library as OPAQUE bodies (the raw
	// PluginKinds["skill"] map). The kernel never reads their fields — CollectSkills
	// (deploykit) projects the composed candies' skills into the ai.opencharly.skill label
	// and candy/plugin-marketplace regenerates the plugins/ corpus from the same source.
	Skills map[string]json.RawMessage `yaml:"skills,omitempty" json:"skills,omitempty"`
	// Namespaces carries child namespaces mounted by namespaced `import:` entries (alias →
	// projected sub-Config). Qualified refs like `cachyos.cachyos` resolve through here.
	// Populated from UnifiedFile.Namespaces by ProjectConfig().
	Namespaces map[string]*Config `yaml:"-"`
}

Config represents the charly.yml configuration projection.

func (*Config) AllBoxNames

func (c *Config) AllBoxNames() []string

AllBoxNames returns every image name (enabled or not), sorted — the raw-map view BoxNames filters. Used by the map-killing accessors where the enabled filter is applied separately. Exported: called cross-package from several charly consumers (host_build_feature.go, resolved_project_host.go, refs.go) and in-package by spec's own distroBuilderMap (builder_resolve.go).

func (*Config) BoxConfig

func (c *Config) BoxConfig(name string) (BoxConfig, bool)

BoxConfig decodes the authored image config for name; ok=false when absent.

func (*Config) BoxNames

func (c *Config) BoxNames() []string

BoxNames returns a sorted list of enabled box names.

func (*Config) EachBox

func (c *Config) EachBox(yield func(string, BoxConfig) bool)

EachBox iterates every image as (name, decoded BoxConfig) in sorted name order — the decode-on-iterate view consumers walk INSTEAD of the kernel holding a typed map. Exported: used cross-package as a range-over-func iterator by charly's validate.go / validate_project_host.go.

func (*Config) FindBoxByLeaf

func (c *Config) FindBoxByLeaf(leaf string) (string, bool)

FindBoxByLeaf searches for a box whose leaf (unqualified) name equals `leaf` — first in c's own root box map, then recursively in each imported namespace (deterministic alias order). Returns the fully-qualified ref under which the box is reachable from c (bare for a root hit, `ns.<...>` for a namespaced hit), or ok=false when no box with that leaf exists anywhere in the namespace tree.

This is the DISCOVERY dual of ResolveBoxRef: ResolveBoxRef resolves a KNOWN qualified ref by descent, whereas FindBoxByLeaf discovers the qualification for a bare leaf that may live in a namespace. ensure-image's build-fallback needs it because it only has the basename of a full registry ref (e.g. `arch-builder` extracted from `ghcr.io/opencharly/arch-builder:<tag>`) and must find that the box lives under the `charly` namespace to build it locally.

func (*Config) HasBox

func (c *Config) HasBox(name string) bool

HasBox reports whether an image named name is present.

func (*Config) ResolveBoxRef

func (c *Config) ResolveBoxRef(ref string) (BoxConfig, *Config, bool)

ResolveBoxRef resolves a (possibly qualified) box name to its BoxConfig and the Config (namespace context) it lives in. Bare names resolve in c; `ns.name` descends into c.Namespaces[ns] recursively. Exported: called cross-package from charly's deploy_ref.go, check_bed_run.go, host_build_box_ref_resolve.go, validate.go, refs.go, and from sdk/buildkit's resolveBase (via the *Config it's given).

func (*Config) SetBox

func (c *Config) SetBox(name string, b BoxConfig)

SetBox stores an authored image config under name (marshaling it opaque).

func (*Config) WalkBaseChain

func (c *Config) WalkBaseChain(boxName string) []BaseChainNode

WalkBaseChain walks boxName's ROOT-INTERNAL base-image chain and returns the images in walk order (self first, then each internal base). It is the ONE shared base-chain traversal used by every chain-walking collector (CollectHooks / CollectShell / CollectDescription / CollectBoxVolume) — each previously re-implemented the identical `for { img := cfg.Box[current]; ...; current = img.Base }` loop (R3: one implementation, no divergent copies), now cycle-safe for all of them.

It deliberately does NOT descend import namespaces. A namespace-qualified base (e.g. `selkies.selkies-labwc`) is a SEPARATELY-BUILT image that owns its own baked check / hooks / shell / volume labels; re-collecting its candies into the consumer would DOUBLE-COUNT every candy the consumer also lists directly. Namespace-AWARENESS belongs to NAME resolution (ResolveBoxRef / FindBoxByLeaf), not to this per-image candy-collection walk; the distro/build VALUE walkers (WalkBaseChainDistro / WalkBaseChainBuild) cross namespaces precisely because those are inherited values, whereas candy contributions are not.

Exported: called cross-package from sdk/deploykit's BoxCandyChain.

func (*Config) WalkBaseChainBuild

func (c *Config) WalkBaseChainBuild(baseName string) []string

WalkBaseChainBuild walks the base chain through box: entries to find the first ancestor with a build: field set. Returns nil if no ancestor defines build formats or the chain reaches an external base image. Exported: called cross-package from sdk/buildkit's resolveBuild.

func (*Config) WalkBaseChainDistro

func (c *Config) WalkBaseChainDistro(baseName string) []string

WalkBaseChainDistro walks the base chain through box: entries to find the first ancestor with a distro: field set. Returns nil if no ancestor defines distro tags or the chain reaches an external base image. Exported: called cross-package from sdk/buildkit's resolveDistro / effectiveBuilderForBox.

type ConstructStepReply

type ConstructStepReply struct {
	Step *InstallStepView `yaml:"step,omitempty" json:"step,omitempty"`
}

type ConstructStepRequest

type ConstructStepRequest struct {
	Op Op `yaml:"op,omitempty" json:"op"`

	CandyName string `yaml:"candy_name,omitempty" json:"candy_name"`

	CandySourceDir string `yaml:"candy_source_dir,omitempty" json:"candy_source_dir,omitempty"`

	CandyVars map[string]string `yaml:"candy_vars,omitempty" json:"candy_vars,omitempty"`

	ResolvedUser string `yaml:"resolved_user,omitempty" json:"resolved_user,omitempty"`

	PkgFormat string `yaml:"pkg_format,omitempty" json:"pkg_format,omitempty"`

	DistroTags []string `yaml:"distro_tags,omitempty" json:"distro_tags,omitempty"`
}

#ConstructStepRequest/#ConstructStepReply — the "construct-step" HostBuild seam (K5-A item 1, compile-seam ctx-threading): the ONE genuinely host-only piece of the former compileActOp — resolving a `run:` act op's `plugin:` word against the PROVIDER REGISTRY (a clause-M kernel mechanism, never reachable from a plugin directly) to decide whether it lowers into a typed InstallStep (TypedStepProvider.ConstructStep, an in-proc-only Go method today — no change), an ExternalPluginStep (an out-of-process executorInvoker verb), an ExternalStep (a class:step provider's declared StepContract), or the generic OpStep fallthrough. Everything ELSE compileActOp used to read off `layer`/`img` is ALREADY portable sdk/deploykit-side (CandyModel, *buildkit.ResolvedBox, deploykit.ResolveUserSpec) and never needs to cross this wire — the caller (deploykit.BuildDeployPlan, now ctx/exec-threaded) resolves those PORTABLE pieces itself and sends only the reduced scalars the registry-consult logic actually reads. The reply carries the constructed step as an OPAQUE #InstallStepView (StepToView/StepFromView, R3 — the SAME wire-view round-trip every other step-carrying seam uses) so the caller re-materializes a real deploykit.InstallStep without a second per-kind decode path. No new #Op selector is needed: the registry-consult decision is HostBuild-KIND-dispatched (string, like every other seam in this file), not a provider-targeted Invoke — compileActOp's logic runs UNCHANGED host-side inside the seam handler.

type Context

type Context = string

--- named-pattern scalars (string) ---

type Copy

type Copy struct {
	Src string `yaml:"src,omitempty" json:"src"`

	Dst string `yaml:"dst,omitempty" json:"dst"`

	Chown bool `yaml:"chown,omitempty" json:"chown,omitempty"`
}

type CredentialHealth

type CredentialHealth struct {
	BackendName string `yaml:"backend_name,omitempty" json:"backend_name"`

	ConfiguredBackend string `yaml:"configured_backend,omitempty" json:"configured_backend"`

	KeyringAvailable bool `yaml:"keyring_available,omitempty" json:"keyring_available"`

	KeyringLocked bool `yaml:"keyring_locked,omitempty" json:"keyring_locked"`

	PlaintextCount int `yaml:"plaintext_count,omitempty" json:"plaintext_count"`

	NoSession bool `yaml:"no_session,omitempty" json:"no_session"`

	CollErr string `yaml:"coll_err,omitempty" json:"coll_err,omitempty"`

	HealthyColls []string `yaml:"healthy_colls,omitempty" json:"healthy_colls,omitempty"`

	BrokenColls []string `yaml:"broken_colls,omitempty" json:"broken_colls,omitempty"`

	IndexTotal int `yaml:"index_total,omitempty" json:"index_total"`

	IndexMissing []string `yaml:"index_missing,omitempty" json:"index_missing,omitempty"`
}

#CredentialHealth is the credential-store health snapshot. Rendered into the doctor "secret storage" checks by the plugin.

type CredentialMount

type CredentialMount struct {
	Src string `yaml:"src,omitempty" json:"src"`

	Dst string `yaml:"dst,omitempty" json:"dst"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Optional bool `yaml:"optional,omitempty" json:"optional,omitempty"`
}

type DataYAML

type DataYAML = CandyData

--- candy sub-types ---

type Debootstrap

type Debootstrap struct {
	Suite string `yaml:"suite,omitempty" json:"suite,omitempty"`

	Mirror string `yaml:"mirror,omitempty" json:"mirror,omitempty"`

	Variant string `yaml:"variant,omitempty" json:"variant,omitempty"`

	Components string `yaml:"components,omitempty" json:"components,omitempty"`

	IncludePackages []string `yaml:"include_package,omitempty" json:"include_package,omitempty"`

	BasePackages []string `yaml:"base_package,omitempty" json:"base_package,omitempty"`

	ExtraRepos []DebootstrapRepo `yaml:"extra_repo,omitempty" json:"extra_repo,omitempty"`
}

type DebootstrapRepo

type DebootstrapRepo struct {
	Name string `yaml:"name,omitempty" json:"name"`

	URL string `yaml:"url,omitempty" json:"url"`

	Suite string `yaml:"suite,omitempty" json:"suite,omitempty"`

	Components string `yaml:"components,omitempty" json:"components,omitempty"`
}

type Deploy

type Deploy struct {
	Version CalVer `yaml:"version,omitempty" json:"version,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	// target is DERIVED from the node's discriminator kind + cross-ref at load
	// (buildFleetNode/inferFleetTarget) — NOT authored in node-form. Optional
	// here so #Check's arms can pin it; the #DeployValue arm (node.cue) rejects an
	// authored `target:` outright. The former default `*"pod"` is dropped (Go's
	// classifyTarget supplies the empty→pod default). Generated as a plain Go
	// `string` (the loader stamps it; the CUE enum still validates a pinned value).
	Target string `yaml:"target,omitempty" json:"target,omitempty"`

	// member_of + inside are loader-DERIVED runtime fields (never authored;
	// rejected by #DeployValue): member_of marks a folded sibling-member entry,
	// inside names the venue a nested resource deploys into. Generated for the Go
	// tree-walker, forbidden in authoring.
	MemberOf string `yaml:"member_of,omitempty" json:"member_of,omitempty"`

	Inside string `yaml:"inside,omitempty" json:"inside,omitempty"`

	// agent_provisioned marks a resource member/child the AI deploys at run time
	// (the iterate-benchmark contract): image-less (no box:), not folded to a
	// top-level entry, exempt from the box-required validators. See deploy.go.
	AgentProvisioned bool `yaml:"agent_provisioned,omitempty" json:"agent_provisioned,omitempty"`

	// descent is the loader-DERIVED venue-hop descriptor (the descent de-type,
	// Cutover H): the substrate plugin stamps it at OpLoad (via kit.StampDescent) so
	// the deploy chain (AppendHopForFlatPath) descends generically BY TRANSPORT,
	// never by switching on the substrate kind word. charly-written state, never
	// authored (rejected by #DeployValue).
	Descent *DescentDescriptor `yaml:"descent,omitempty" json:"descent,omitempty"`

	// EDGE-INHERIT cutover B: the substrate kind is the EDGE discriminator (pod:/vm:/
	// kubernetes:/local:/android:/group:), so the deploy carries only NON-kind cross-refs:
	//
	//	from  — inherit a SAME-kind template by name (vm/kubernetes/local/android deploys).
	//	image — the box/OCI artifact a pod/kubernetes/android RUNS (the former `box:`).
	//
	// Per-substrate validity (image⊻from, source⊻from) is enforced in Go
	// (classifyTarget / validateDeploy), not CUE, so a `vm:` node is a VmSpec template
	// (source:) OR a deploy (from:) under ONE arm — the disjunction #Vm|#Deploy.
	From EntityRef `yaml:"from,omitempty" json:"from,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

	Replica int `yaml:"replica,omitempty" json:"replica,omitempty"`

	Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`

	Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty"`

	Tunnel *TunnelYAML `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`

	DNS string `yaml:"dns,omitempty" json:"dns,omitempty"`

	AcmeEmail string `yaml:"acme_email,omitempty" json:"acme_email,omitempty"`

	Port []PortPin `yaml:"port,omitempty" json:"port,omitempty"`

	ResolvedPort []PortPin `yaml:"resolved_port,omitempty" json:"resolved_port,omitempty"`

	// resolved_image: the concrete image ref the pod deploy's add_candy: overlay
	// build produced (`<deploy-key>-overlay:<hash>`), persisted by PrepareVenue so
	// config/start deploy EXACTLY that overlay (carrying the add_candy layers)
	// instead of re-resolving the base image: short-name by a CalVer sort that the
	// overlay alias can lose to the base on a same-minute build. charly-written
	// state (like resolved_port), never authored; empty for a plain pod.
	ResolvedImage string `yaml:"resolved_image,omitempty" json:"resolved_image,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	Secret []DeploySecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	Volume []DeployVolume `yaml:"volume,omitempty" json:"volume,omitempty"`

	// volume_project_checked marks that this deploy's PROJECT-declared `volume:` override has
	// already been consulted ONCE (true regardless of whether the project declared one), so
	// resolveDeployVolumes (candy/plugin-deploy-pod) never re-queries the project on a
	// subsequent `charly config`/`charly update` merely because the per-host overlay's Volume
	// field happens to be empty (that emptiness is ALSO produced by an unrelated overlay write —
	// e.g. the port-resolution block — that never touched volumes at all, so key-existence alone
	// cannot distinguish "checked, found nothing" from "not checked yet"). charly-written state
	// (like resolved_port/resolved_image), never authored.
	VolumeProjectChecked bool `yaml:"volume_project_checked,omitempty" json:"volume_project_checked,omitempty"`

	// The Go type is OPAQUE (map[string]json.RawMessage): CUE still validates each
	// per-deploy sidecar override against #Sidecar, but the kernel stores it as
	// opaque bodies — ALL sidecar business logic lives in candy/plugin-sidecar's
	// OpResolve (the sidecar de-type, Cutover D). The host never reads Sidecar fields.
	Sidecar map[string]RawBody `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`

	ForwardGpgAgent *bool `yaml:"forward_gpg_agent,omitempty" json:"forward_gpg_agent,omitempty"`

	ForwardSSHAgent *bool `yaml:"forward_ssh_agent,omitempty" json:"forward_ssh_agent,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Iterate *Iterate `yaml:"iterate,omitempty" json:"iterate,omitempty"`

	// record — the bed-level whole-run recording wrap (G5 follow-up): when set,
	// the check-run runner starts a recording on the venue before the live
	// phases and stops it after, landing the artifact in the bed's run evidence
	// (recordings.yml beside summary.yml). terminal/desktop select the recorder
	// lane; fps + record_env pass through to the record verb (desktop sessions
	// on VM venues need XDG_RUNTIME_DIR/WAYLAND_DISPLAY).
	Record *Deploy_RecordWrap `yaml:"record,omitempty" json:"record,omitempty"`

	AddCandy []string `yaml:"add_candy,omitempty" json:"add_candy,omitempty"`

	InstallOpts *InstallOpts `yaml:"install_opts,omitempty" json:"install_opts,omitempty"`

	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	SSHArgs []string `yaml:"ssh_arg,omitempty" json:"ssh_arg,omitempty"`

	Cpus int `yaml:"cpus,omitempty" json:"cpus,omitempty"`

	Ram VmSize `yaml:"ram,omitempty" json:"ram,omitempty"`

	DiskSize VmSize `yaml:"disk_size,omitempty" json:"disk_size,omitempty"`

	Deploy *KubernetesDeploy `yaml:"deploy,omitempty" json:"deploy,omitempty"`

	Resources *DeployResources `yaml:"resources,omitempty" json:"resources,omitempty"`

	Expose *DeployExpose `yaml:"expose,omitempty" json:"expose,omitempty"`

	Storage []DeployStorage `yaml:"storage,omitempty" json:"storage,omitempty"`

	Probes *DeployProbes `yaml:"probes,omitempty" json:"probes,omitempty"`

	FromSnapshot string `yaml:"from_snapshot,omitempty" json:"from_snapshot,omitempty"`

	CloudInitClean bool `yaml:"cloud_init_clean,omitempty" json:"cloud_init_clean,omitempty"`

	VmState *VmDeployState `yaml:"vm_state,omitempty" json:"vm_state,omitempty"`

	// snapshot — the check-bed snapshot-anchoring policy (§5.3.1): capture at
	// install finalize and reset before every check run, so a batch of PR runs
	// shares ONE golden disk (revert ≈ seconds vs fresh install ≈ 20-30 min).
	// VM-only (the substrate-word checks reject it on other substrates).
	Snapshot *VmSnapshotPolicy `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`

	// variants — named VM-config overrides that boot the SAME golden disk with a
	// different shape (cpus/memory/video/gpu/display/devices/attachments). A
	// variant may ONLY change VM shape — any change to source: or disk identity
	// is rejected, because the disk comes exclusively from the shared snapshot
	// chain. The unnamed default variant equals the bed's own vm: attributes.
	Variants map[string]*VmVariant `yaml:"variants,omitempty" json:"variants,omitempty"`

	// update_gate — the check-bed's declarative R10 fresh-update change-class
	// switch: how the Step-5 acceptance gate re-verifies the bed (the declarative
	// twin of `charly check run --no-rebuild`). Check-bed only (like snapshot:);
	// meaningless on a plain deploy.
	//
	//	full (default)         — the canonical destroy+recreate gate: `charly
	//	                         update` rebuilds the venue from scratch (a VM
	//	                         domain is destroyed + recreated, a pod image
	//	                         rebuilt + container recreated, a group rebuilds
	//	                         every member image) and re-checks it.
	//	restart-only           — replace the destroy/recreate with a plain venue
	//	                         RESTART (a VM BOOTs its existing clone again, a
	//	                         pod restarts its container, a group cycles its
	//	                         members WITHOUT rebuilding images) and re-runs
	//	                         the check-live pass against the restarted
	//	                         venue. The change class for runtime-PR-injection
	//	                         software evals: a rebuild cannot alter a
	//	                         runtime-injected artifact, so the full gate
	//	                         spends its destroy/recreate time (~38s in the
	//	                         measured omarchy PR-eval lane) proving nothing.
	//	skip                   — no update phase and no post-update pass (the
	//	                         declarative twin of --no-rebuild). --no-rebuild
	//	                         / --anchor still force skip structurally.
	//
	// In-place (local/external) beds have no restartable venue: restart-only
	// leaves the gate step unchanged there (the in-place re-apply), same as full.
	UpdateGate string `yaml:"update_gate,omitempty" json:"update_gate,omitempty"`

	Disposable *bool `yaml:"disposable,omitempty" json:"disposable,omitempty"`

	Lifecycle string `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"`

	Ephemeral *EphemeralLifetime `yaml:"ephemeral,omitempty" json:"ephemeral,omitempty"`

	Preemptible *PreemptibleConfig `yaml:"preemptible,omitempty" json:"preemptible,omitempty"`

	RequiresExclusive []string `yaml:"requires_exclusive,omitempty" json:"requires_exclusive,omitempty"`

	RequiresShared []string `yaml:"requires_shared,omitempty" json:"requires_shared,omitempty"`

	// nested/peer map keys carry no dots (validateDeploymentName). Loader-built
	// runtime tree maps (Children = nested-inside venue; Members = brought-up
	// alongside on the shared network) — gengotypes can't express a pattern-keyed
	// self-referential map, so the Go type is pinned explicitly.
	Children map[string]*Deploy `yaml:"nested,omitempty" json:"nested,omitempty"`

	Members map[string]*Deploy `yaml:"peer,omitempty" json:"peer,omitempty"`
}

func (*Deploy) HasChildren

func (n *Deploy) HasChildren() bool

HasChildren reports whether this node has any nested deployments.

func (Deploy) IsDisposable

func (c Deploy) IsDisposable() bool

IsDisposable reports whether the deploy is safe to destroy + rebuild.

func (Deploy) IsEphemeral

func (c Deploy) IsEphemeral() bool

IsEphemeral reports whether this deploy is marked ephemeral.

func (Deploy) IsGroup

func (c Deploy) IsGroup() bool

IsGroup reports whether this is a GROUP fleet — no workload cross-ref but with sibling members (the cross-deployment subject+driver shape).

func (Deploy) IsPreemptible

func (c Deploy) IsPreemptible() bool

IsPreemptible reports whether this deploy holds a preemptible exclusive resource (graceful-stop side).

func (Deploy) LifecycleTag

func (c Deploy) LifecycleTag() string

LifecycleTag returns the deploy's lifecycle tag.

func (Deploy) PreemptionHolds

func (c Deploy) PreemptionHolds() []string

PreemptionHolds returns the exclusive-resource token(s) this deploy occupies.

func (Deploy) RequiredExclusive

func (c Deploy) RequiredExclusive() []string

RequiredExclusive returns the exclusive-resource token(s) this deploy claims.

func (Deploy) RequiredShared

func (c Deploy) RequiredShared() []string

RequiredShared returns the shared-mode resource token(s) this deploy claims.

type DeployCompileReply

type DeployCompileReply struct {
	PlansJSON RawBody `yaml:"plans,omitempty" json:"plans"`

	Base string `yaml:"base,omitempty" json:"base,omitempty"`

	CandySet []string `yaml:"candy_set,omitempty" json:"candy_set,omitempty"`
}

#DeployCompileReply is the OpCompile reply: the compiled plans as marshalled []spec.InstallPlanView (a hand-written sdk/spec wire type with no CUE def → opaque RawBody envelope; the host unmarshals into []spec.InstallPlanView and re-materializes []*spec.InstallPlan via deploykit.PlanFromView), plus the base identity (box name) and the resolved candy set (the order, for deployID + overlay-candy propagation).

type DeployCompileRequest

type DeployCompileRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir"`

	BoxView ResolvedBoxView `yaml:"box_view,omitempty" json:"box_view,omitempty"`

	Order []string `yaml:"order,omitempty" json:"order,omitempty"`

	HostContextJSON RawBody `yaml:"host_context,omitempty" json:"host_context"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	// The add_candy:/--add-candy ref(s) (if any) this compile call's own candy set was widened
	// with host-side (scanCandiesForRef's synthetic-augmented scan, for a REMOTE ref) — threaded
	// into the plugin's OWN InvokeProvider("build","project") re-fetch (as its extra_candy_refs) so
	// the envelope's candy map ALSO carries them (RCA'd K1-alpha regression: the two scans were
	// independent, so a remote add-candy resolved host-side never reached the envelope).
	ExtraCandyRefs []string `yaml:"extra_candy_refs,omitempty" json:"extra_candy_refs,omitempty"`

	// candy_ref selects the CANDY shape above (K4 unit B): the authored ref string (bare local
	// name OR a `@github…` remote ref) the plugin resolves via BareRef against its own
	// rp.CandyModels/rp.Candies (widened, when remote, by extra_candy_refs carrying the SAME raw
	// ref — mirroring compileStandaloneCandySelection's ExtraCandyRefs: []string{ref.Raw}
	// widening). Absent for the other shapes.
	CandyRef string `yaml:"candy_ref,omitempty" json:"candy_ref,omitempty"`

	// vm_entity selects the CANDY shape's synthetic-box kind, TOLERANTLY (mirrors the OLD host
	// syntheticVmBox call site exactly — never a hard requirement): the plugin tries vm_entity
	// against its own rp.Templates.VM; a HIT resolves it via the kind:vm provider's OpResolve leg
	// for a guest-tuned box; a MISS (including vm_entity=="") falls through to a plain host-adhoc
	// box via vmshared.DetectHostDistro. A non-vm deploy's node.From (e.g. a `local:` node's
	// kind:local template ref) is ALSO threaded into vm_entity upstream (resolveVmEntity returns
	// node.From unconditionally, not only for a real vm cross-ref) — so a miss here is the
	// COMMON case, not an error condition. Ignored for the other shapes.
	VmEntity string `yaml:"vm_entity,omitempty" json:"vm_entity,omitempty"`

	// box_ref selects the BOX-REF shape above (K4 unit B box-half): the box's own name (never a
	// remote ref — compileRefSelection already rejects a remote image ref before this request is
	// built). The plugin's own InvokeProvider("build","project") re-fetch is asked to include_disabled
	// so an explicitly-named `enabled: false` box still resolves (mirrors the OLD host
	// ResolveBox(cfg, ref.Name, …) call, which never checked IsEnabled at all — enabled-filtering
	// is a ResolveAllBox/listing concern, not a by-name-resolve one; zero disabled boxes exist
	// repo-wide today, so this is a zero-cost future-proofing widening, not a live behavior
	// change). Absent for the other shapes.
	BoxRef string `yaml:"box_ref,omitempty" json:"box_ref,omitempty"`

	// base_box_ref selects the ADD-CANDY-ON-BOX shape (K4 box-half completion) WHEN set alongside
	// candy_ref: the primary pod/kubernetes base image's own name, read from rp.Boxes[base_box_ref] as the
	// COMPILE CONTEXT the candy_ref overlay compiles against — replacing the former host-side
	// buildkit.ResolveBox(baseImg) + scanCandiesForRef. Absent for every other shape (a standalone
	// candy_ref with NO base_box_ref stays the CANDY shape, synthetic-box compiled).
	BaseBoxRef string `yaml:"base_box_ref,omitempty" json:"base_box_ref,omitempty"`
}

#DeployCompileRequest is the per-node COMPILE seam (K4-B / K4 unit B): the host asks the command:fleet plugin's OpCompile handler to compile, in one of THREE selection SHAPES (a discriminated set, not three Ops — R3). The plugin fetches the resolved-project envelope itself via InvokeProvider("build","project", OpResolve) peer-dispatch (the former HostBuild("resolved-project") seam is DELETED — it does NOT receive the whole project in the request), loops deploykit.BuildDeployPlan over the resolved order, and returns []InstallPlanView. The host re-materializes []*InstallPlan from the views via deploykit.PlanFromView.

  • BOX-VIEW selection (an already-resolved base image, ctx!=nil): box_view + order are POPULATED host-side (the host projects an ALREADY-RESOLVED base image via projectResolvedBox) and the plugin trusts them as sent. Retained for any caller that still resolves the base image host-side; the add_candy-on-pod/kubernetes path itself now uses the ADD-CANDY-ON-BOX shape below.
  • ADD-CANDY-ON-BOX selection (compileCandyOnBoxSelection, the add_candy-on-pod/kubernetes shape, K4 box-half completion): candy_ref (the add_candy overlay ref) AND base_box_ref (the primary pod/kubernetes base image name) are BOTH set — the plugin reads rp.Boxes[base_box_ref] (the SAME ResolvedBoxView the primary BOX-REF shape reads, R3) as the COMPILE CONTEXT, resolves the add_candy's OWN topo order from rp.CandyModels (deploykit.ResolveCandyOrder over {BareRef(candy_ref)}, widened by extra_candy_refs for a remote overlay), prunes container-init-for-systemd, and compiles that order against the base image — replacing the former host-side buildkit.ResolveBox(baseImg) + scanCandiesForRef path.
  • CANDY selection (compileStandaloneCandySelection, the target:local/vm standalone-candy shape, K4 unit B candy-half): candy_ref is set — the plugin resolves the candy key + topo order itself from its own envelope (deploykit.ResolveCandyOrder over rp.CandyModels, already pure) and builds ITS OWN synthetic box (vmshared.DetectHostDistro/DetectHostGlibc for a host target — already sdk-portable; the kind:vm provider's own OpResolve leg for a vm target, mirroring the kind:local OpResolve reuse in node_resolve.go's lookupLocalTemplate, K4 unit A).
  • BOX-REF selection (compileBoxSelection, the primary pod/kubernetes image shape, K4 unit B box-half): box_ref is set — the plugin reads rp.Boxes[box_ref] (the SAME ResolvedBoxView hostBuildResolvedProject already computed to BUILD the envelope in the first place — no re-derivation, R3) directly via deploykit.NewSpecResolvedBox, and resolves the candy topo order itself from img.Candy over rp.CandyModels (deploykit.ResolveCandyOrder, same as the CANDY shape).

Exactly one of {box_view, box_ref, candy_ref-alone} is set, OR the ADD-CANDY-ON-BOX pair (candy_ref + base_box_ref) together. Neither CANDY nor BOX-REF needs LoadUnified or the provider-CONNECT registry (verified live, K4 unit B: candy/plugin-fleet's own ALREADY-EXISTING preresolveBuilderContexts, called unconditionally for every OpCompile, already S2-lazy-connects any externalized builder the resolved order+img trigger via exec.InvokeProvider — an exhaustive repo grep found zero target:local/vm or pod/kubernetes deploy anywhere needing a builder plugin outside the calling project's own candy closure, the one edge S2's Pass-1 project-scan can't cover), so neither needs a new HostBuild kind.

HostContextJSON is the marshalled spec.HostContext — the 4 host-computed WIRE scalars (MachineVenue/Distro/GlibcVersion/BuilderImage) — a hand-written spec type with no CUE def (the gengotypes spike cannot express its json:"-" in-process fields), so it rides as an opaque RawBody envelope (the VmJSON/PodConfigJSON idiom; the plugin unmarshals into spec.HostContext, which sdk/deploykit re-exports as deploykit.HostContext). ONLY the 4 scalars cross: the plugin populates the json:"-" BuilderContext (preresolveBuilderContexts over the reverse channel) + ActiveInit (off the resolved-project envelope's rp.Init) IN-PROCESS after the decode — never on the wire. Tag is the image CalVer pin (for the plan Version field when set). Dir is the project dir the plugin threads into its InvokeProvider("build","project") call (empty → plugin cwd).

type DeployContext added in v0.2026218.138

type DeployContext struct {
	// Node is the dispatch-merged FleetNode. nil for a ref-based
	// deploy with no charly.yml entry (e.g. `charly fleet add host ./x.yml`).
	Node *FleetNode

	// Name is the deploy key (the bed key / charly.yml map key, e.g.
	// "check-k3s-vm"). Distinct from the kind:vm entity name (node.From).
	Name string

	// Dir is the project directory.
	Dir string

	// Cfg / DistroCfg / BuilderCfg are the configs loaded once by the
	// resolve-target-add host seam. Reused by each Add so the construction
	// matches what the plugin compiled plans against.
	Cfg        *Config
	DistroCfg  *DistroConfig
	BuilderCfg *BuilderConfig
}

DeployContext carries everything an Add needs from the generic dispatchNode pre-stage: the dispatch-merged FleetNode (the project+operator field-level merged deploy tree — the SINGLE source of truth for node fields like Nested/Env/ephemeral/disposable, NEVER re-read inside an Add), the deploy name + project dir, the loaded image/distro/builder configs, and the resolved primary base ref. One value threaded into every target.Add so each adapter constructs its live embedded target without re-resolving config that dispatchNode already loaded.

type DeployDelResolveReply

type DeployDelResolveReply struct {
	Node *Deploy `yaml:"node,omitempty" json:"node,omitempty"`

	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
}

type DeployDelResolveRequest

type DeployDelResolveRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	// tree_json is the merged project+operator deploy tree the command:fleet plugin already
	// resolved PLUGIN-SIDE (resolveTreeViaLoader, which also connects the deployment's plugins) —
	// threaded as DATA so the host resolveDelNode consumes it instead of re-loading the tree
	// host-side (#55 Cone A Unit 3a). Marshalled map[string]spec.Deploy; an absent/empty tree
	// falls through to resolveDelNode's non-tree fallbacks (vm-prefix / pod-artifact probe), exactly
	// as a nil host-tree-read result did before.
	TreeJSON RawBody `yaml:"tree_json,omitempty" json:"tree_json,omitempty"`
}

#DeployDelResolveRequest/#DeployDelResolveReply — resolve a `charly fleet del` target's FleetNode (resolveDelNode: literal "host" / "vm:"-prefix legacy forms / a charly.yml tree entry / a ref-based pod-artifact probe) — needs LoadUnified + the on-disk artifact probe, so it stays host-side; the plugin's `charly fleet del` calls this FIRST.

type DeployExecutor

type DeployExecutor interface {
	// Venue returns a stable identifier for where this executor's
	// commands physically run. Examples:
	//
	//   "local"                            — ShellExecutor.
	//   "ssh://arch@127.0.0.1:2224"        — SSHExecutor.
	//   "nested:podman exec stack/local"   — NestedExecutor over local.
	//   "nested:ssh vm/local"              — NestedExecutor over SSH.
	//
	// The string is used as a map key for per-venue ledgers, so it
	// must be stable across invocations for the same logical target.
	// Not a URL — don't parse it; just compare.
	Venue() string

	// RunSystem executes a shell script with root privileges. On the
	// host, this is `sudo bash -s <<<script`; on the VM target, it's
	// `ssh <user>@<host> sudo bash -s <<<script`. The script body runs
	// with set -e semantics at the caller's discretion.
	//
	// The interpreter is NOT guaranteed to be bash. Those two implementations
	// pin bash, but NestedExecutor selects the far-side interpreter at run time
	// inside the target and degrades to `sh` on a base that ships no bash (see
	// exec.nestedShellProbe). A script body handed to this interface must
	// therefore be POSIX sh — a bashism runs on the host and silently fails
	// inside a busybox container or guest.
	RunSystem(ctx context.Context, script string, opts EmitOpts) error

	// RunUser executes a shell script as the invoking user (no sudo).
	// On the host, it's `bash -s <<<script`; on VM, `ssh <user>@<host>
	// bash -s <<<script` where <user> is the unprivileged guest user.
	// Same interpreter caveat as RunSystem — write POSIX sh.
	RunUser(ctx context.Context, script string, opts EmitOpts) error

	// RunBuilder invokes the multi-stage builder image (podman run
	// <builder>) to compile pixi/npm/cargo/aur artifacts. On the host
	// this calls the existing BuilderRun helper. On VM deploys, the
	// builder runs *on the host* and artifacts are scp'd into the
	// guest via PutFile — podman inside the guest is not required.
	RunBuilder(ctx context.Context, opts BuilderRunOpts) ([]byte, error)

	// PutFile places a file at a remote path. ownerRoot == true means
	// the file is chown'd to root:root and chmod'd according to mode.
	// On the host, this is a plain os.WriteFile (plus sudo chown when
	// ownerRoot). On VM, this is scp into a tmp location followed by
	// `sudo install -m <mode> -o root -g root` on the guest.
	PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts EmitOpts) error

	// GetFile retrieves the contents of a file on the venue. asRoot==true
	// runs the read via sudo to handle paths the deploying user cannot
	// access (e.g. /etc/rancher/k3s/k3s.yaml on a k3s server). On the
	// host, this is os.ReadFile (or `sudo cat` when asRoot). On VM, this
	// is `ssh <host> sudo cat <path>` with stdout captured. On nested
	// executors, delegates through the jump via the parent's own RunSystem
	// semantics. Used by layer_artifacts.go to publish files back to the
	// operator after deploy completion.
	GetFile(ctx context.Context, remotePath string, asRoot bool, opts EmitOpts) ([]byte, error)

	// RunCapture executes a single shell command (or short POSIX-sh script) on
	// the venue and returns stdout/stderr/exit/err separately. Used by the
	// declarative test runner (testrun.go) to probe target state without
	// the streamed-output ergonomics of RunSystem/RunUser. No root
	// escalation — callers add `sudo` explicitly when needed; mirrors the
	// previous test-time Executor.Exec semantics. After the executor-
	// hierarchy cutover (2026-04), this is the single capture-output
	// method used by every probe across `charly check live`, `charly check box`, and
	// `charly check` scoring.
	RunCapture(ctx context.Context, script string) (stdout, stderr string, exit int, err error)

	// RunInteractive runs a command on the venue wired to the operator's LIVE TTY:
	// stdin/stdout/stderr are INHERITED from the host charly process (the reverse-channel
	// server runs IN that process, so os.Std* IS the operator's terminal). The child
	// (`podman exec -it` / `ssh -t`) owns the PTY, resize (SIGWINCH), and Ctrl-C. Blocks
	// until the session ends; returns its exit code. NOT ctx-deadlined — the TTY owns its
	// lifetime (the hostBuildCli doctrine). Stdio NEVER crosses the plugin wire — only the
	// script + the exit code (F12; the live-stdio sibling of RunCapture). Consumers: `charly
	// shell` (-it) and `charly cmd` (-i, stdin piped). A venue that cannot host an interactive
	// session returns ErrNotSupported.
	RunInteractive(ctx context.Context, script string) (exit int, err error)

	// RunStream runs a command on the venue streaming stdout/stderr LIVE to the operator
	// (inherited os.Stdout/os.Stderr; NO stdin). Blocks until exit. Same host-holds-the-
	// terminal semantics as RunInteractive (F12). Consumer: `charly logs --follow`. Returns
	// ErrNotSupported on a venue that cannot stream.
	RunStream(ctx context.Context, script string) (exit int, err error)

	// Kind returns a coarse classification of the venue used by the test
	// runner for reporting and skip decisions. Values:
	//   "host"      — ShellExecutor (operator's machine)
	//   "container" — NestedExecutor with JumpPodmanExec / JumpDockerExec
	//   "vm"        — SSHExecutor or NestedExecutor with JumpSSH/JumpVirshConsole
	// Replaces the test-time Executor.Kind() method deleted in the
	// 2026-04 executor-hierarchy cutover.
	Kind() string

	// ResolveHome returns the absolute path of $HOME for the named user
	// on the venue. Empty user means "the executor's default user" (the
	// invoking operator for ShellExecutor; the SSH login user for
	// SSHExecutor). Implementations consult `getent passwd` so they
	// don't depend on $HOME being set in the calling environment — that
	// matters for SSH executors where the operator's $HOME has nothing
	// to do with the remote user's home, and for ShellExecutor when the
	// caller wants a different user's home (e.g. running as root but
	// resolving an unprivileged user's home).
	//
	// Bundled as part of the 2026-05 shell:-schema cutover. Replaces the
	// `the local deploy target.HostHome = os.Getenv("HOME")` static-field
	// initialization that mis-targeted SSH deploys: the operator's
	// $HOME is not the remote user's home, so every shell-rc edit
	// (env.d sourcing block included) was landing in the wrong place
	// for `host: user@machine` deploys.
	ResolveHome(ctx context.Context, user string) (string, error)
}

DeployExecutor is the in-process host-side executor: the concrete venue an InstallPlan's steps are applied against (ShellExecutor on the operator's machine, SSHExecutor over ssh, NestedExecutor through a jump). The three implementations live in sdk/kit; the InstallPlan IR (EmitOpts.ParentExec) carries one through the nested-deploy tree. DISTINCT from kit.DeployExecutor (the reverse-channel executor a plugin serves over gRPC) — same role, different wire shape (typed EmitOpts + file paths here; JSON-marshalled opts + byte content there).

func ReconstructParentExec added in v0.2026218.138

func ReconstructParentExec(ancestorPaths []string, ancestorNodes []FleetNode, derive func(string, *FleetNode, DeployExecutor) (DeployExecutor, error)) (DeployExecutor, error)

ReconstructParentExec re-derives the ancestor executor chain from ROOT-FIRST ancestor path/node lists (spec/fleet.ResolveNodePath's contract, EXCLUDING the target itself), applying derive to each ancestor. derive is the registry-coupled per-ancestor hop (core's deriveChildExecutorForPath) — the loop itself is pure, so it lives here once (K-wave 2 cone R2 bank D thin: the resolve-target-add seam's former reconstructParentExec).

type DeployExpose

type DeployExpose struct {
	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`

	TLS bool `yaml:"tls,omitempty" json:"tls,omitempty"`

	Port string `yaml:"port,omitempty" json:"port,omitempty"`
}

type DeployNodeDelDispatchReply

type DeployNodeDelDispatchReply struct {
}

type DeployNodeDelDispatchRequest

type DeployNodeDelDispatchRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Node *Deploy `yaml:"node,omitempty" json:"node,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	KeepRepoChanges bool `yaml:"keep_repo_changes,omitempty" json:"keep_repo_changes,omitempty"`

	KeepServices bool `yaml:"keep_services,omitempty" json:"keep_services,omitempty"`

	KeepImage bool `yaml:"keep_image,omitempty" json:"keep_image,omitempty"`

	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`
}

#DeployNodeDelDispatchRequest/#DeployNodeDelDispatchReply — the `charly fleet del` terminal step: ResolveTarget + target.Del, honoring the teardown gates (the live ReverseRunner is still never carried on the wire — a programmatic teardown needing a specific runner is resolved host-side during dispatch).

type DeployOverlayReply

type DeployOverlayReply struct {
	ConfigJSON RawBody `yaml:"config_json,omitempty" json:"config_json,omitempty"`
}

#DeployOverlayReply carries the marshaled per-host FleetConfig. config_json is the JSON encoding of *deploykit.FleetConfig (nil-safe: absent/null when no per-host overlay file exists yet, matching LoadDeployConfigForRead's own nil-FleetConfig contract).

type DeployOverlayRequest

type DeployOverlayRequest struct {
	Context string `yaml:"context,omitempty" json:"context"`
}

#DeployOverlayRequest asks the host for the PER-HOST deploy-config overlay (K4: deploykit.LoadDeployConfigForRead — the runtime ledger at ~/.config/charly/charly.yml, NOT the project charly.yml the resolved-project envelope projects; Mode Purity keeps the two apart, same distinction #PodDisposableRequest documents). Unlike #PodDisposableRequest (a single overlay BIT), several pod-lifecycle resolvers need CROSS-DEPLOYMENT visibility (deploykit.FleetConfig's GlobalEnvForImage/OccupiedHostPorts/DeployedContainerNames all read OTHER deploys' entries, not just the caller's own), so a single-field extraction can't serve them — the host returns the WHOLE marshaled *deploykit.FleetConfig and the plugin calls the SAME already-portable deploykit methods locally. Re-fetched on EVERY call (no caching): the ledger can change between invocations (an intervening `charly config`), and PrepareVenue-time data is stale by the time OpStart/OpStop/OpShell run much later — this is NOT threaded through the one-shot LifecyclePrepareInput. Class-generic action noun "deploy-overlay" (F11 — never a substrate word).

type DeployPluginsConnectReply

type DeployPluginsConnectReply struct {
	Dir string `yaml:"dir,omitempty" json:"dir"`
}

type DeployPluginsConnectRequest

type DeployPluginsConnectRequest struct {
	Path string `yaml:"path,omitempty" json:"path"`

	AddCandy []string `yaml:"add_candy,omitempty" json:"add_candy,omitempty"`
}

#DeployPluginsConnectRequest/#DeployPluginsConnectReply — the K1-LOADER RELOCATION witness (Unit D). candy/plugin-fleet now DRIVES loaderkit.LoadUnified ITSELF, plugin-side, over the reverse-channel LoaderExecutor (execLoaderExecutor → the "loader-*" host legs), to resolve the `charly fleet add` deploy tree — the host no longer runs a host-side merged-tree read for the walk. This seam is the ONE host-only PREAMBLE the plugin still needs: connect the deployment's out-of-tree plugin candies (loadDeployPlugins — registry-coupled, a core Mechanism) BEFORE ResolveTarget can route to an external substrate, and return the resolved project dir (host os.Getwd — the SAME dir the host loader uses) the plugin passes to loaderkit.LoadUnified. The plugin reads root-venue-ssh itself from the tree's stamped node.Descent (loaderkit.LoadUnified stamps it), so no host trait call — proving plugin-fleet → loaderkit.LoadUnified end-to-end.

type DeployProbes

type DeployProbes struct {
	Liveness *Op `yaml:"liveness,omitempty" json:"liveness,omitempty"`

	Readiness *Op `yaml:"readiness,omitempty" json:"readiness,omitempty"`

	Startup *Op `yaml:"startup,omitempty" json:"startup,omitempty"`
}

DeployProbes (deploy.go) — each probe is an inline Op (the check verb vocab).

type DeployRecord added in v0.2026215.1748

type DeployRecord struct {
	// schema_version is the ledger-format version (the ledger-candy-keys cutover's
	// CalVer). Empty means a pre-cutover record (json "layer" keys) — the read path
	// rejects it with a `charly migrate` hint.
	SchemaVersion string `yaml:"schema_version,omitempty" json:"schema_version,omitempty"`

	DeployID string `yaml:"deploy_id,omitempty" json:"deploy_id"`

	Image string `yaml:"image,omitempty" json:"image"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Target string `yaml:"target,omitempty" json:"target"`

	Candy []string `yaml:"candy,omitempty" json:"candy,omitempty"`

	AddCandy []string `yaml:"add_candy,omitempty" json:"add_candy,omitempty"`

	DeployedAt string `yaml:"deployed_at,omitempty" json:"deployed_at"`
}

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

type DeployReply

type DeployReply struct {
	ReverseOps []ReverseOp `yaml:"reverse_ops,omitempty" json:"reverse_ops,omitempty"`

	Record DeployReplyRecord `yaml:"record,omitempty" json:"record"`
}

#DeployReply is the structured result an external deploy provider returns from an OpExecute Invoke: the teardown ops the host records into the ledger, plus a provenance record.

type DeployReplyRecord

type DeployReplyRecord struct {
	Candy string `yaml:"candy,omitempty" json:"candy"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`
}

#DeployReplyRecord names the ledger CandyRecord the host writes for an external deploy: the logical candy whose ReverseOps drive teardown, plus its version.

type DeployResolveTargetAddReply

type DeployResolveTargetAddReply struct {
}

type DeployResolveTargetAddRequest

type DeployResolveTargetAddRequest struct {
	Path string `yaml:"path,omitempty" json:"path"`

	DeployName string `yaml:"deploy_name,omitempty" json:"deploy_name"`

	Node *Deploy `yaml:"node,omitempty" json:"node,omitempty"`

	Target string `yaml:"target,omitempty" json:"target"`

	Dir string `yaml:"dir,omitempty" json:"dir"`

	// plans_json is the marshalled []spec.InstallPlanView (deployID + AddCandies already stamped
	// plugin-side); the host re-materializes []*spec.InstallPlan via deploykit.PlanFromView.
	PlansJSON RawBody `yaml:"plans_json,omitempty" json:"plans_json"`

	AncestorPaths []string `yaml:"ancestor_paths,omitempty" json:"ancestor_paths,omitempty"`

	AncestorNodes []Deploy `yaml:"ancestor_nodes,omitempty" json:"ancestor_nodes,omitempty"`

	NodeOnly bool `yaml:"node_only,omitempty" json:"node_only,omitempty"`

	Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"`

	Verify bool `yaml:"verify,omitempty" json:"verify,omitempty"`

	WithServices bool `yaml:"with_services,omitempty" json:"with_services,omitempty"`

	AllowRepoChanges bool `yaml:"allow_repo_changes,omitempty" json:"allow_repo_changes,omitempty"`

	AllowRootTasks bool `yaml:"allow_root_tasks,omitempty" json:"allow_root_tasks,omitempty"`

	SkipIncompatible bool `yaml:"skip_incompatible,omitempty" json:"skip_incompatible,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	BuilderImage string `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`

	// dev_local_pkg marks a DISPOSABLE CHECK BED's deploy, the deploy-side twin of `charly box
	// build --dev-local-pkg`. On a bed, a localpkg candy whose package source cannot be found is
	// a HARD FAILURE rather than the benign skip a normal deploy takes — a bed exists to prove
	// the in-development package builds and installs, so silently installing nothing (or an
	// older release) makes the bed assert something it never tested.
	DevLocalPkg bool `yaml:"dev_local_pkg,omitempty" json:"dev_local_pkg,omitempty"`
}

#DeployResolveTargetAddRequest/#DeployResolveTargetAddReply — the K4-C SHAPE-2 per-node terminal step: ResolveTarget + DeployContext + target.Add for ONE tree position, reached once per node from the plugin's own walk. UNLIKE the retired #DeployNodeDispatchRequest (which had the host RE-COMPILE the plans via an in-proc OpCompile round-trip — the plugin→host→plugin double-bounce), the plugin now COMPILES the InstallPlans IN-PROC (walk.go dispatchOne → compileNodePlans → compilePlansForRequest, no OpCompile hop) and ships the ALREADY-COMPILED plans (with deployID + AddCandies stamped plugin-side, round-tripped through InstallPlanView) as plans_json. The host half does ONLY the genuine floor-M residue a plugin cannot: reconstruct the ancestor executor chain (deriveChildExecutorForPath — registry-coupled), loadConfigForDeploy (LoadUnified), ResolveTarget + DeployContext + utgt.Add.

ancestor_paths/ancestor_nodes let the host reconstruct the SAME parentExec chain the OLD in-core walk built (deriveChildExecutorForPath is pure Go over spec/kit types, re-run HOST-side) — a live DeployExecutor never crosses the wire. target is the plugin-classified substrate word (a pure spec/fleet.ClassifyNodeTarget of node+path), carried so the host synthesizes a Target-only node when node is nil (a ref-based deploy with no charly.yml entry). The gate flags are the FINAL resolved EmitOpts values (node.InstallOpts already applied over the CLI flags plugin-side); dry_run never reaches this seam — a dry-run prints the compiled plans plugin-side and returns without dispatch.

type DeployResources

type DeployResources struct {
	CPURequest string `yaml:"cpu_request,omitempty" json:"cpu_request,omitempty"`

	MemoryRequest string `yaml:"memory_request,omitempty" json:"memory_request,omitempty"`
}

type DeploySecret

type DeploySecret struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Source string `yaml:"source,omitempty" json:"source,omitempty"`
}

type DeployStorage

type DeployStorage struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Size string `yaml:"size,omitempty" json:"size,omitempty"`

	ClassHint string `yaml:"class_hint,omitempty" json:"class_hint,omitempty"`

	Access string `yaml:"access,omitempty" json:"access,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`
}

type DeployTargetDelOpts

type DeployTargetDelOpts struct {
	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	KeepLedger bool `yaml:"keep_ledger,omitempty" json:"keep_ledger,omitempty"`

	RemoveVolumes bool `yaml:"remove_volumes,omitempty" json:"remove_volumes,omitempty"`

	KeepRepoChanges bool `yaml:"keep_repo_changes,omitempty" json:"keep_repo_changes,omitempty"`

	KeepServices bool `yaml:"keep_services,omitempty" json:"keep_services,omitempty"`

	KeepImage bool `yaml:"keep_image,omitempty" json:"keep_image,omitempty"`
}

#DeployTargetDelOpts is the `charly fleet del` opts type — formerly the charly-core DelOpts, now CUE-sourced (the UnifiedDeployTarget contract lives in spec/spec/deploy_target_unified.go). The three teardown gates (KeepRepoChanges/KeepServices/KeepImage) were folded into DelOpts proper in S3b, replacing the pre-S3b type-assertion in host_build_deploy_node_del_dispatch.go; the del dispatcher now passes them as plain fields.

type DeployTargetDispatchReply

type DeployTargetDispatchReply struct {
	Status DeployTargetStatus `yaml:"status,omitempty" json:"status,omitempty"`

	Output string `yaml:"output,omitempty" json:"output,omitempty"`

	ExitCode int64 `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`

	VenueJSON RawBody `yaml:"venue_json,omitempty" json:"venue_json,omitempty"`

	ArtifactKey string `yaml:"artifact_key,omitempty" json:"artifact_key,omitempty"`
}

#DeployTargetDispatchReply carries whatever the dispatched op produces — Status for "status", Output/ExitCode for "shell"/"attach" (the F12 non-zero-exit propagation via *sdk.ExitCodeError), Venue for "add"/"update" (the spec.VenueDescriptor PrepareVenue produced or re-confirmed, threaded back to core so a substrate WITH a lifecycle hook — pod's overlay container, vm's guest — hands core the SAME live executor for --verify and every subsequent dispatch on this target, mirroring the pre-S3b `t.exec = exec` reassignment inside apply()), nothing for the rest (success is "no error").

type DeployTargetDispatchRequest

type DeployTargetDispatchRequest struct {
	Op string `yaml:"op,omitempty" json:"op"`

	Name string `yaml:"name,omitempty" json:"name"`

	Word string `yaml:"word,omitempty" json:"word"`

	HasLifecycle bool `yaml:"has_lifecycle,omitempty" json:"has_lifecycle,omitempty"`

	HasPreresolve bool `yaml:"has_preresolve,omitempty" json:"has_preresolve,omitempty"`

	HasPlan bool `yaml:"has_plan,omitempty" json:"has_plan,omitempty"`

	Node *Deploy `yaml:"node,omitempty" json:"node,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	NodeOnly bool `yaml:"node_only,omitempty" json:"node_only,omitempty"`

	// ledger_root OPTIONALLY overrides the ledger root directory (kit.LedgerPaths.Root) — the
	// pre-S3b externalDeployTarget carried a settable `paths *kit.LedgerPaths` field a TEST could
	// redirect to a temp dir instead of the operator's real ~/.config/opencharly/installed/; this
	// is the wire-safe equivalent (a bare root string, the plugin derives Deploys/Candies/LockFile
	// from it exactly like kit.DefaultLedgerPaths does). Empty (the default) — kit.DefaultLedgerPaths().
	LedgerRoot string `yaml:"ledger_root,omitempty" json:"ledger_root,omitempty"`

	PlansJSON RawBody `yaml:"plans_json,omitempty" json:"plans_json,omitempty"`

	OptsJSON RawBody `yaml:"opts_json,omitempty" json:"opts_json,omitempty"`

	ChecksJSON RawBody `yaml:"checks_json,omitempty" json:"checks_json,omitempty"`

	Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"`

	TTY bool `yaml:"tty,omitempty" json:"tty,omitempty"`

	// venue_json is the ALREADY-MATERIALIZED spec.VenueDescriptor for this deploy. Two distinct
	// producers set it: (a) core, when this dispatch is a NESTED non-lifecycle child (a
	// `local:`/`android:`/`kubernetes:` deploy under a vm/pod, tree position) — Add flattens the
	// live ancestor executor (EmitOpts.ParentExec) into this field via kit.DescriptorFromExecutor
	// BEFORE the very first "add" dispatch, since that live value cannot itself cross the wire
	// (FIX ROUND, S3b follow-up — its absence on "add" was the R10 bed regression: every nested
	// child silently applied on the operator's host instead of the parent venue); (b) core again,
	// carrying FORWARD a prior dispatch's reported venue for the SAME target's lifetime
	// (Update/Del/Start/Stop/Status/Logs/Shell/Attach/Rebuild, after a lifecycle substrate's "add"
	// already ran PrepareVenue once). Either way the plugin re-materializes it via
	// kit.VenueFromDescriptor instead of re-deriving from the node or re-running PrepareVenue.
	// Absent on "add" for a ROOT (non-nested) OR lifecycle (vm/pod) target — those derive their
	// own venue fresh (root: RootExecutorForDeployNode(node); lifecycle: PrepareVenue).
	VenueJSON RawBody `yaml:"venue_json,omitempty" json:"venue_json,omitempty"`

	// distro_cfg_json is the marshalled *buildkit.DistroConfig ("add"/"update" only) — recordDeploy's
	// FillReverseUninstallCmds needs it to render an aur-builder ReverseOpPackageRemove's
	// UninstallCmd host-side-equivalent, now plugin-side since buildkit.DistroConfig is a plain sdk
	// type with no core-only coupling (unlike the core-only buildEngineContext wrapper it used to
	// travel inside).
	DistroCfgJSON RawBody `yaml:"distro_cfg_json,omitempty" json:"distro_cfg_json,omitempty"`

	// host_env_json is the marshalled spec.HostEnv (CharlyBin/Home/Version) — the R10 bed-found
	// fifth bug in this cluster's move: every lifecycle Op the deleted substrate_lifecycle_grpc.go
	// sent used its OWN hostEnvJSON() helper, computed HOST-side (os.Executable() resolves to the
	// charly binary only when called IN CORE — a plugin's os.Executable() resolves to the PLUGIN
	// binary, wrong for an out-of-process placement even though today's compiled-in placement
	// happened not to crash on it, since a bare `spec.HostEnv{}` zero-value was marshalled instead
	// of ever actually calling it, plugin-side, in the S3b port). Core (unified_targets.go's
	// dispatch, the ONLY place that reliably knows its OWN binary regardless of the substrate's or
	// command:fleet's own placement) now computes it ONCE per dispatch call and threads it here;
	// candy/plugin-fleet forwards it verbatim to every lifecycle Op instead of computing its own.
	HostEnvJSON RawBody `yaml:"host_env_json,omitempty" json:"host_env_json,omitempty"`
}

#DeployTargetDispatchRequest (S3b) is the ONE generic host→command:fleet envelope every UnifiedDeployTarget/LifecycleTarget method dispatches through, discriminated by `op` (the project rulebook's "generic over ad-hoc" — one wire shape, not eleven). Core's thin ResolveTarget proxy (unified_targets.go) constructs this per call from data alone — it never holds a *grpcProvider or any core-private registry object, so the type is free to live entirely on the wire. `word` is the resolved deploy-substrate provider word (e.g. "pod"/"vm"/ "local"/"kubernetes"/"android") the plugin dispatches the ACTUAL substrate leg to, via its own sdk.Executor.InvokeProvider — core never talks to the substrate provider directly once this lands. `has_lifecycle` is the ONE piece of substrate metadata core must resolve itself (the registered-provider's own `lifecycle` flag lives on the core-private *grpcProvider) — it gates whether Start/Stop/Status/Logs/Shell/Attach/Rebuild are even valid for this substrate (mirroring the former ErrNotSupportedOnExternal branches) AND whether the Q1 arbiter bracket applies to Start/Stop — `has_plan` is that DIFFERENT, narrower boolean (K4-exit, FLOOR-SLIM- proper Unit-8): core still computes it (lifecycleStartPlanHooks[word]/lifecycleStopPlanHooks[word] presence, pod_lifecycle_dispatch.go, unmoved) and now THREADS it on the wire instead of bracketing the dispatch call itself — command:fleet's handleLifecycleSimple owns the bracket call, by InvokeProvider("verb","arbiter") peer dispatch (the "arbiter-bracket-*" HostBuild seams are DELETED, K-wave 2 cone R2 bank E; the CHARLY_PREEMPT_LEASE os.Setenv/os.Getenv nested-subprocess-inheritance property lives in candy/plugin-preempt's invokeArbiter + the plugin-fleet bracket, both compiled-in sharing the host process). `node` is the dispatch-merged FleetNode (nil for a ref-based deploy with no charly.yml entry) — required when has_plan is true (the claim fields live on it). `plans_json` carries []InstallPlanView for Add/Update. `opts_json` carries the op-specific opts struct as an opaque envelope (the zero-value-safe pattern every #Pod*Opts request already uses) — kept opaque rather than one field per opts type so a NEW deploy verb never needs a NEW CUE field, only a new decode branch plugin-side.

type DeployTargetLogsOpts

type DeployTargetLogsOpts struct {
	Follow bool `yaml:"follow,omitempty" json:"follow,omitempty"`

	Tail int64 `yaml:"tail,omitempty" json:"tail,omitempty"`

	Sidecar string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

#DeployTargetLogsOpts is the `charly logs` opts type — formerly the charly-core LogsOpts, now CUE-sourced (the UnifiedDeployTarget contract lives in spec/spec/deploy_target_unified.go).

type DeployTargetRebuildOpts

type DeployTargetRebuildOpts struct {
	RebuildImage bool `yaml:"rebuild_image,omitempty" json:"rebuild_image,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`
}

#DeployTargetRebuildOpts is the `charly update` rebuild-path opts type — formerly the charly-core RebuildOpts, now CUE-sourced (the UnifiedDeployTarget contract lives in spec/spec/deploy_target_unified.go). `tag` pins the image the rebuild re-deploys to (the `charly update --tag` flag); empty = newest local CalVer resolved via the ai.opencharly.version OCI label.

type DeployTargetStatus

type DeployTargetStatus struct {
	State string `yaml:"state,omitempty" json:"state,omitempty"`

	Healthy bool `yaml:"healthy,omitempty" json:"healthy,omitempty"`

	Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
}

#DeployTargetStatus is the live-runtime state for the "status" deploy op — formerly the charly-core StatusInfo, now CUE-sourced because it crosses the plugin boundary; the UnifiedDeployTarget contract (spec/spec/deploy_target_unified.go) uses this type directly.

type DeployTraits

type DeployTraits struct {
	// venue: how commands physically execute in this substrate's venue:
	//
	//	container — podman/docker exec into the container by name (pod).
	//	ssh       — an ssh hop into the guest (vm).
	//	shell     — the substrate's own root executor runs on the host (local; kubernetes host-side).
	//	parent    — reached via the parent's venue, no own executor (android).
	//	none      — external-in-place (zero value / group).
	Venue string `yaml:"venue,omitempty" json:"venue,omitempty"`

	// image_backed: the substrate runs a baked OCI image (pod).
	ImageBacked bool `yaml:"image_backed,omitempty" json:"image_backed,omitempty"`

	// image_context: the substrate composes over an image build context (pod overlay, kubernetes manifests).
	ImageContext bool `yaml:"image_context,omitempty" json:"image_context,omitempty"`

	// machine_venue: the substrate is a full machine with a system init (host/vm/local) — its
	// services render as systemd units, not a container init.
	MachineVenue bool `yaml:"machine_venue,omitempty" json:"machine_venue,omitempty"`

	// exclusive_venue: the substrate holds an exclusive host-resource lease boundary (vm).
	ExclusiveVenue bool `yaml:"exclusive_venue,omitempty" json:"exclusive_venue,omitempty"`

	// leaf_only: the substrate is a deploy-chain LEAF — it cannot be descended into (kubernetes).
	LeafOnly bool `yaml:"leaf_only,omitempty" json:"leaf_only,omitempty"`

	// bracketed_lifecycle: the substrate's Start/Stop accept direct-mode CLI opts (env/port/
	// volume/bind/no-auto-detect on Start, unmount on Stop) AND need the Q1 resource-arbiter
	// claim bracketed around the dispatch (pod). A substrate that manages its own venue
	// lifecycle + resource claim (vm, via `charly vm start`/`stop`) leaves this false.
	BracketedLifecycle bool `yaml:"bracketed_lifecycle,omitempty" json:"bracketed_lifecycle,omitempty"`

	// bed_target: the substrate is a valid `kind:check` bed target — pod/vm/local/android run a
	// disposable check bed; kubernetes (leaf_only) does not. validateCheckBeds reads this instead of
	// switching on the substrate word (the boundary-law incomplete-seam gate).
	BedTarget bool `yaml:"bed_target,omitempty" json:"bed_target,omitempty"`

	// supports_ephemeral: the substrate's Add/Del path wires the ephemeral register/teardown seam
	// (TTL timer + charly.yml persistence) — vm only today; pod/kubernetes reject `ephemeral: true` until
	// their Add/Del wire the same seam. validateEphemeral reads this instead of a substrate switch.
	SupportsEphemeral bool `yaml:"supports_ephemeral,omitempty" json:"supports_ephemeral,omitempty"`

	// supports_from_snapshot: the substrate has a backing chain `from_snapshot:` restores from — vm
	// only (pod/kubernetes have no backing chains). validateEphemeral reads this instead of a word switch.
	SupportsFromSnapshot bool `yaml:"supports_from_snapshot,omitempty" json:"supports_from_snapshot,omitempty"`
}

#DeployTraits is a SUBSTRATE kind's DECLARED deploy behaviour (P9): a substrate plugin advertises it per word over Describe (ProvidedCapability.deploy_traits), and kit.StampDescent stamps it onto every node's #DescentDescriptor. It is the SINGLE plugin-declared source for "how does this substrate behave in the deploy chain", so the kernel consults the traits off node.Descent BY TRAIT — never by switching on the substrate kind word (the boundary law). Canonical table: pod=container+image_backed+image_context; vm=ssh+machine_venue+exclusive_venue; local=shell+machine_venue; kubernetes=shell+image_context+leaf_only; android=parent; zero value = external-in-place. Not authored — charly-written state stamped at load.

type DeployTraitsCarrier

type DeployTraitsCarrier interface {
	DeclaredDeployTraits() *DeployTraits
}

DeployTraitsCarrier is implemented by a provider (grpcProvider out-of-proc, inprocProvider compiled-in) that carries a SUBSTRATE class:kind capability's DECLARED DeployTraits (P9). Non-nil → deployTraitsFor returns them so kit.StampDescent stamps node.Descent BY TRAIT; nil (or not implemented) → the external-in-place default. This is the SINGLE plugin-declared source for a substrate's deploy behaviour — the consult sites read the stamped traits off node.Descent, never switching on the substrate kind word (the kernel/plugin boundary law).

type DeployVenue

type DeployVenue struct {
	DeployName string `yaml:"deploy_name,omitempty" json:"deploy_name"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Substrate RawBody `yaml:"substrate,omitempty" json:"substrate,omitempty"`
}

#DeployVenue is the venue descriptor the host puts in op.Env for an external deploy Invoke: the deploy's name plus the merged deploy-node env (KEY=VALUE lines flattened to a map).

type DeployVolume

type DeployVolume struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`

	DataSeeded bool `yaml:"data_seeded,omitempty" json:"data_seeded,omitempty"`

	DataSource string `yaml:"data_source,omitempty" json:"data_source,omitempty"`
}

type Deploy_RecordWrap added in v0.2026245.1613

type Deploy_RecordWrap struct {
	Terminal bool `yaml:"terminal,omitempty" json:"terminal,omitempty"`

	Desktop bool `yaml:"desktop,omitempty" json:"desktop,omitempty"`

	Fps int64 `yaml:"fps,omitempty" json:"fps,omitempty"`

	Record_name string `yaml:"record_name,omitempty" json:"record_name,omitempty"`

	Settle_ms int64 `yaml:"settle_ms,omitempty" json:"settle_ms,omitempty"`

	Record_env map[string]string `yaml:"record_env,omitempty" json:"record_env,omitempty"`

	Artifact string `yaml:"artifact,omitempty" json:"artifact,omitempty"`

	Artifact_min_bytes int64 `yaml:"artifact_min_bytes,omitempty" json:"artifact_min_bytes,omitempty"`
}

RecordWrap is the whole-run recording wrap (deploy record: field).

type DeploymentStatus

type DeploymentStatus struct {
	Kind SubstrateKind `yaml:"kind,omitempty" json:"kind"`

	Image string `yaml:"image,omitempty" json:"image"`

	ImageRef string `yaml:"image_ref,omitempty" json:"image_ref,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Status string `yaml:"status,omitempty" json:"status"`

	Uptime string `yaml:"uptime,omitempty" json:"uptime,omitempty"`

	Container string `yaml:"container,omitempty" json:"container"`

	Ports []PortMapping `yaml:"ports,omitempty" json:"ports,omitempty"`

	Devices []string `yaml:"devices,omitempty" json:"devices,omitempty"`

	Tools []ToolStatus `yaml:"tools,omitempty" json:"tools,omitempty"`

	Volumes []string `yaml:"volumes,omitempty" json:"volumes,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	Tunnel string `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`

	Secrets []string `yaml:"secrets,omitempty" json:"secrets,omitempty"`

	RunMode string `yaml:"run_mode,omitempty" json:"run_mode"`

	Nested []*DeploymentStatus `yaml:"nested,omitempty" json:"nested,omitempty"`

	Source string `yaml:"source,omitempty" json:"source,omitempty"`
}

#DeploymentStatus — the rendered shape for the table + JSON outputs across every deployment substrate (pod / vm / kubernetes / local / android). kind discriminates the substrate; nested carries multi-hop children (RECURSIVE self-reference, populated by the nested overlay); source records provenance (libvirt|ledger|adb|tree|podman).

type DescentDescriptor

type DescentDescriptor struct {
	// transport: how the deploy chain descends INTO this node's substrate (DERIVED from the
	// declared venue + leaf_only by kit.DescentFromTraits):
	//
	//	none           — shares the parent venue; no hop (local, android).
	//	container-exec — enter the container by name (pod; podman/docker per engine).
	//	ssh            — an ssh hop into the guest (vm).
	//	reject         — unreachable via the deploy chain (kubernetes → use kubectl).
	Transport string `yaml:"transport,omitempty" json:"transport"`

	// host_rooted: the substrate's own ROOT executor runs directly on the host
	// (local), so the check runner uses rootExecutorForDeployNode, not a container chain.
	HostRooted bool `yaml:"host_rooted,omitempty" json:"host_rooted,omitempty"`

	// venue: how commands physically execute in this substrate's venue:
	//
	//	container — podman/docker exec into the container by name (pod).
	//	ssh       — an ssh hop into the guest (vm).
	//	shell     — the substrate's own root executor runs on the host (local; kubernetes host-side).
	//	parent    — reached via the parent's venue, no own executor (android).
	//	none      — external-in-place (zero value / group).
	Venue string `yaml:"venue,omitempty" json:"venue,omitempty"`

	// image_backed: the substrate runs a baked OCI image (pod).
	ImageBacked bool `yaml:"image_backed,omitempty" json:"image_backed,omitempty"`

	// image_context: the substrate composes over an image build context (pod overlay, kubernetes manifests).
	ImageContext bool `yaml:"image_context,omitempty" json:"image_context,omitempty"`

	// machine_venue: the substrate is a full machine with a system init (host/vm/local) — its
	// services render as systemd units, not a container init.
	MachineVenue bool `yaml:"machine_venue,omitempty" json:"machine_venue,omitempty"`

	// exclusive_venue: the substrate holds an exclusive host-resource lease boundary (vm).
	ExclusiveVenue bool `yaml:"exclusive_venue,omitempty" json:"exclusive_venue,omitempty"`

	// leaf_only: the substrate is a deploy-chain LEAF — it cannot be descended into (kubernetes).
	LeafOnly bool `yaml:"leaf_only,omitempty" json:"leaf_only,omitempty"`

	// bracketed_lifecycle: the substrate's Start/Stop accept direct-mode CLI opts (env/port/
	// volume/bind/no-auto-detect on Start, unmount on Stop) AND need the Q1 resource-arbiter
	// claim bracketed around the dispatch (pod). A substrate that manages its own venue
	// lifecycle + resource claim (vm, via `charly vm start`/`stop`) leaves this false.
	BracketedLifecycle bool `yaml:"bracketed_lifecycle,omitempty" json:"bracketed_lifecycle,omitempty"`

	// bed_target: the substrate is a valid `kind:check` bed target — pod/vm/local/android run a
	// disposable check bed; kubernetes (leaf_only) does not. validateCheckBeds reads this instead of
	// switching on the substrate word (the boundary-law incomplete-seam gate).
	BedTarget bool `yaml:"bed_target,omitempty" json:"bed_target,omitempty"`

	// supports_ephemeral: the substrate's Add/Del path wires the ephemeral register/teardown seam
	// (TTL timer + charly.yml persistence) — vm only today; pod/kubernetes reject `ephemeral: true` until
	// their Add/Del wire the same seam. validateEphemeral reads this instead of a substrate switch.
	SupportsEphemeral bool `yaml:"supports_ephemeral,omitempty" json:"supports_ephemeral,omitempty"`

	// supports_from_snapshot: the substrate has a backing chain `from_snapshot:` restores from — vm
	// only (pod/kubernetes have no backing chains). validateEphemeral reads this instead of a word switch.
	SupportsFromSnapshot bool `yaml:"supports_from_snapshot,omitempty" json:"supports_from_snapshot,omitempty"`
}

#DescentDescriptor is the loader-DERIVED venue-hop descriptor (Cutover H + P9). candy/plugin-substrate declares its per-word #DeployTraits over Describe, and kit.StampDescent stamps them here so the deploy chain descends generically by TRANSPORT and every consult site reads the substrate behaviour off the stamped TRAITS — never by switching on the substrate kind word. transport/host_rooted are the DERIVED nesting view (from the traits); the closed transport set is the kernel's nesting-boundary vocabulary.

func DescentFromTraits added in v0.2026213.1726

func DescentFromTraits(t *DeployTraits) *DescentDescriptor

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

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

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

type DesktopCategory added in v0.2026242.935

type DesktopCategory string

#DesktopCategory is the freedesktop registered MAIN categories, verbatim. Closed on purpose — see the note on `categories`.

type DesktopEntry added in v0.2026242.935

type DesktopEntry struct {
	// entry_name is the file stem and the menu label source.
	EntryName string `yaml:"entry_name,omitempty" json:"entry_name"`

	// exec and url are mutually exclusive: `url` renders the browser --app=<url> form, which
	// is all a "web app" installer does. Enforced by OpValidate rather than CUE so the error
	// can say which one to remove.
	Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`

	URL string `yaml:"url,omitempty" json:"url,omitempty"`

	// browser_arg are extra flags for the url form.
	BrowserArgs []string `yaml:"browser_arg,omitempty" json:"browser_arg,omitempty"`

	Icon DesktopIcon `yaml:"icon,omitempty" json:"icon,omitempty"`

	// categories is CLOSED to the freedesktop registered main categories. A typo here does
	// not error anywhere — the entry silently vanishes from every menu — which is exactly
	// the class of failure a closed enum exists to catch.
	Categories []DesktopCategory `yaml:"categories,omitempty" json:"categories,omitempty"`

	MimeTypes []string `yaml:"mime_type,omitempty" json:"mime_type,omitempty"`

	// startup_wm_class ties the launched window back to this entry. Also read by the session
	// renderer's window rules, which is this kind's reason to exist.
	StartupWMClass string `yaml:"startup_wm_class,omitempty" json:"startup_wm_class,omitempty"`

	Window DesktopWindow `yaml:"window,omitempty" json:"window,omitempty"`

	Terminal bool `yaml:"terminal,omitempty" json:"terminal,omitempty"`

	Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}

#DesktopEntry is a freedesktop .desktop file.

A .desktop file is a trivial INI, and this kind earns its keep on ONE ground: startup_wm_class and window are consumed by the SESSION renderer through the render context, so a single entity feeds both the applications menu and the compositor's window rules. Without that it would be `write:` duplication and should be dropped.

type DesktopIcon added in v0.2026242.935

type DesktopIcon struct {
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Source string `yaml:"source,omitempty" json:"source,omitempty"`
}

#DesktopIcon is name XOR source: a stock icon name, or a candy-relative file that lowers to `copy:` (an icon base64'd through a plan is neither readable nor small).

type DesktopWindow added in v0.2026242.935

type DesktopWindow struct {
	Placement string `yaml:"placement,omitempty" json:"placement,omitempty"`

	Workspace string `yaml:"workspace,omitempty" json:"workspace,omitempty"`

	Size string `yaml:"size,omitempty" json:"size,omitempty"`
}

#DesktopWindow is the placement the session renderer turns into a window rule.

type DetectedDevices

type DetectedDevices struct {
	GPU bool `yaml:"gpu,omitempty" json:"gpu"`

	AMDGPU bool `yaml:"amd_gpu,omitempty" json:"amd_gpu"`

	AMDGFXVersion string `yaml:"amd_gfx_version,omitempty" json:"amd_gfx_version"`

	RenderNode string `yaml:"render_node,omitempty" json:"render_node"`

	Devices []string `yaml:"devices,omitempty" json:"devices"`
}

#DetectedDevices holds the results of host device auto-detection.

type Diagnostic

type Diagnostic struct {
	Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`

	Message string `yaml:"message,omitempty" json:"message"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`
}

#Diagnostic is one finding from a plugin kind's deep OpValidate check (F7/C8).

type Diagnostics

type Diagnostics struct {
	Items []Diagnostic `yaml:"items,omitempty" json:"items,omitempty"`
}

#Diagnostics is the OpValidate reply. HasErrors() is a pure Go METHOD — CUE cannot express it — and stays hand-written in spec/deploy_methods.go.

func (Diagnostics) HasErrors

func (d Diagnostics) HasErrors() bool

HasErrors reports whether any item is error-severity (empty severity counts as error).

type DiscoverConfig added in v0.2026210.808

type DiscoverConfig []ScanSpec

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

type DiscoveredManifest

type DiscoveredManifest struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	Manifest string `yaml:"manifest,omitempty" json:"manifest,omitempty"`

	RootDir string `yaml:"rootDir,omitempty" json:"rootDir,omitempty"`

	Docs []ParsedProject `yaml:"docs,omitempty" json:"docs,omitempty"`
}

#DiscoveredManifest — one discovered entity directory's manifest (a `discover:` walk hit), applied by the host AFTER the docs (explicit-entry-wins). `dir`/`rootDir`/`manifest` let the host register a lazy candy `From:` reference (a LAYER candy) or materialize the node (any other kind); `docs` are the manifest's parsed documents.

type DiskLayout added in v0.2026242.753

type DiskLayout struct {
	// esp_mount_point is where the EFI System Partition is mounted, relative to the guest
	// root. Defaults to "/boot/efi".
	//
	// This is NOT cosmetic. Omarchy mounts its ESP at "/boot", which is what
	// limine-entry-tool and ESP_PATH assume; a loader written to the other path leaves an
	// unbootable disk and the build reports success either way.
	EspMountPoint string `yaml:"esp_mount_point,omitempty" json:"esp_mount_point,omitempty"`

	// subvolume, when non-empty, makes the root filesystem a btrfs subvolume layout
	// instead of a bare filesystem. Requires the VM source's rootfs to be "btrfs".
	//
	// Exactly one entry must mount at "/" — it becomes the root that the others nest
	// under, so without it there is nothing to mount them into. Enforced by OpValidate
	// rather than by CUE, because "exactly one element of this list has field X = Y" is
	// not expressible in a closed struct.
	Subvolume []Subvolume `yaml:"subvolume,omitempty" json:"subvolume,omitempty"`
}

#DiskLayout describes how a bootstrap VM's disk is partitioned and mounted, for the distros whose on-disk shape is part of their identity rather than a per-VM choice.

Both fields are optional and both default to what charly did before this def existed, so a distro that omits the block builds exactly the disk it built before: a bare root filesystem with the ESP at /boot/efi.

type DisplayManager added in v0.2026242.935

type DisplayManager struct {
	Manager string `yaml:"manager,omitempty" json:"manager"`

	// session is the #SessionDesktop.id to start. Cross-checked at load.
	Session string `yaml:"session,omitempty" json:"session"`

	Autologin AutoLogin `yaml:"autologin,omitempty" json:"autologin,omitempty"`

	Numlock string `yaml:"numlock,omitempty" json:"numlock,omitempty"`

	// theme names a greeter theme package or directory. Free-form: every greeter spells
	// this differently and none of them validate it either.
	Theme string `yaml:"theme,omitempty" json:"theme,omitempty"`

	// config is the greeter's own files, rendered the same way a theme's are.
	Config []ThemeRender `yaml:"config,omitempty" json:"config,omitempty"`

	// unit is the systemd unit to enable. Named explicitly rather than derived from
	// `manager`, because a distro may ship it under a different name and guessing would be
	// the "renderer guesses the distro" failure this codebase already recorded.
	Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`
}

#DisplayManager is the greeter: which one, which session it starts, and whether it logs a user in without asking.

The load-time cross-check is the point: `session` must match a #SessionDesktop.id that some session entity in the same project declares. An autologin pointing at a session file nothing installed produces a black screen and an empty journal, which is the single worst failure this surface has.

type Distro

type Distro struct {
	Inherits string `yaml:"inherits,omitempty" json:"inherits,omitempty"`

	InheritPackages bool `yaml:"inherit_packages,omitempty" json:"inherit_packages,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Bootstrap Bootstrap `yaml:"bootstrap,omitempty" json:"bootstrap,omitempty"`

	Workarounds []string `yaml:"workaround,omitempty" json:"workaround,omitempty"`

	Format map[string]*Format `yaml:"format,omitempty" json:"format,omitempty"`

	BaseUser *BaseUser `yaml:"base_user,omitempty" json:"base_user,omitempty"`

	Pacstrap *Pacstrap `yaml:"pacstrap,omitempty" json:"pacstrap,omitempty"`

	Debootstrap *Debootstrap `yaml:"debootstrap,omitempty" json:"debootstrap,omitempty"`

	AlpineBootstrap *AlpineBootstrap `yaml:"alpine_bootstrap,omitempty" json:"alpine_bootstrap,omitempty"`

	Bootloader *Bootloader `yaml:"bootloader,omitempty" json:"bootloader,omitempty"`

	DiskLayout *DiskLayout `yaml:"disk_layout,omitempty" json:"disk_layout,omitempty"`

	Dnf *Dnf `yaml:"dnf,omitempty" json:"dnf,omitempty"`

	Installer *DistroInstaller `yaml:"installer,omitempty" json:"installer,omitempty"`
}

func (*Distro) LocalPkgFormat

func (d *Distro) LocalPkgFormat(primaryFormat string) (string, *LocalPkg)

LocalPkgFormat picks the format whose local_pkg block drives the charly package INSTALL (the download leg): the caller's primary format, then the distro's own PrimaryFormat, then any localpkg-capable format (deterministic). Shares the selection core with the *ResolvedDistro twin (spec/distro_methods.go).

func (*Distro) PrimaryFormat

func (d *Distro) PrimaryFormat() string

PrimaryFormat returns the distro's primary (non-secondary) package format. Shares the selection core with the *ResolvedDistro twin (spec/distro_methods.go).

type DistroBuilderCandidate added in v0.2026213.1726

type DistroBuilderCandidate struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Distro []string `yaml:"distro,omitempty" json:"distro"`

	Builder BuilderMap `yaml:"builder,omitempty" json:"builder"`
}

#DistroBuilderCandidate is one named entity's Distro tags + Builder map — the generic shape PickDistroBuilder consumes (R3: the shared abstraction behind the "distro-keyed builder default" lookup). Pure data (string/[]string/#BuilderMap) with zero buildkit-specific typing, so it is CUE-sourced here alongside #BuilderMap per the same rule — moved out of buildkit (the builder-map value-primitive cluster: EffectiveBuilderForBox/ResolveEffectiveBuilder/ distroBuilderMap/PickDistroBuilder + this candidate type relocated to spec/spec as pure-value CONTRACT computation, plan Rule 2 + step 3). The lookup FUNCTIONS themselves are BEHAVIOR (hand-written Go, not CUE-generated) and live in spec/spec/builder_resolve.go.

type DistroConfig added in v0.2026210.2052

type DistroConfig struct {
	Distro map[string]*ResolvedDistro `yaml:"distro,omitempty" json:"distro"`
}

#DistroConfig is the `distro:` build-vocabulary container — the whole distro map keyed by distro name (charly/charly.yml's `distro:` section). #55 step 3-III: relocated from sdk/buildkit so every charly/plugin file that uses ONLY this value type drops its sdk/buildkit import for spec (the import-purity lever). Its vocabulary-resolution methods (ResolveDistro / ResolveInherits / AllFormatNames / FindFormat / ValidFormat / ExpandPackageInheritance) are pure Go METHODS on the type's own map — CUE cannot express them — and stay hand-written in spec/distro_config_methods.go (mirrors ResolvedDistro's PrimaryFormat in spec/distro_methods.go: a method, not a type).

func ProjectDistroConfig added in v0.2026213.1726

func ProjectDistroConfig(uf *UnifiedFile, resolveDistro func(json.RawMessage) (*ResolvedDistro, error)) *DistroConfig

ProjectDistroConfig reconstructs the *DistroConfig (distro: section) from uf, resolving each opaque `distro` body via the caller-supplied resolveDistro callback (the distro plugin's OpResolve leg). Nil when no distros are configured.

func WrapDistroDef added in v0.2026210.2052

func WrapDistroDef(def *ResolvedDistro) *DistroConfig

WrapDistroDef presents one already-resolved ResolvedDistro as a DistroConfig so the format-keyed FindFormat resolver returns that def's Format. Returns nil for a nil def.

func (*DistroConfig) AllFormatNames added in v0.2026210.2052

func (dc *DistroConfig) AllFormatNames() []string

AllFormatNames returns a sorted, deduplicated list of all format names across all distros.

func (*DistroConfig) ExpandPackageInheritance added in v0.2026210.2052

func (dc *DistroConfig) ExpandPackageInheritance(tags []string) []string

ExpandPackageInheritance appends, AFTER the authored distro tags, every `inherits:` ancestor of a tag whose distro def opts into package inheritance via `inherit_packages: true` — most-specific authored tags kept first, ancestors appended as the least-specific levels.

func (*DistroConfig) FindFormat added in v0.2026210.2052

func (dc *DistroConfig) FindFormat(name string) *Format

FindFormat returns the Format for a format name (rpm/deb/pac/aur), resolving distro `inherits:` chains. The first distro that defines the format wins. Returns nil when no distro defines it.

func (*DistroConfig) ResolveDistro added in v0.2026210.2052

func (dc *DistroConfig) ResolveDistro(distroTags []string) *ResolvedDistro

ResolveDistro finds the distro definition matching the image's distro tags. Walks tags in order, strips :version suffix to match base distro name. Follows inherits: chains with cycle detection, inheriting formats from parent.

func (*DistroConfig) ResolveInherits added in v0.2026210.2052

func (dc *DistroConfig) ResolveInherits(def *ResolvedDistro, maxDepth int) *ResolvedDistro

func (*DistroConfig) ValidFormat added in v0.2026210.2052

func (dc *DistroConfig) ValidFormat(name string) bool

ValidFormat returns true if any distro defines this format name.

type DistroDef

type DistroDef = Distro

--- top-level entity types ---

type DistroInstaller added in v0.2026241.2320

type DistroInstaller struct {
	// volume_id is the filesystem LABEL the installer looks for. archinstall and
	// cloud-init NoCloud use "cidata"; Anaconda kickstart uses "OEMDRV". Case is
	// preserved verbatim by the ISO writer, so write it exactly as the installer
	// matches it.
	VolumeID string `yaml:"volume_id,omitempty" json:"volume_id"`

	// fs is how the answers volume is packed. iso9660 is correct for every installer
	// verified so far; vfat exists for one that matches its label case-sensitively in
	// a way ISO 9660 cannot represent.
	FS string `yaml:"fs,omitempty" json:"fs,omitempty"`

	Files []DistroInstallerFile `yaml:"file,omitempty" json:"file"`

	// boot_arg is appended to the INSTALLER's kernel cmdline when the installer needs
	// to be pointed at its answers explicitly ("inst.ks=hd:LABEL=OEMDRV:/ks.cfg").
	// Empty for an installer that auto-discovers the labelled volume.
	BootArg string `yaml:"boot_arg,omitempty" json:"boot_arg,omitempty"`

	// done is how the build path learns the install finished. poweroff: the installer
	// powers the guest off and the build waits for the process to exit. marker: the
	// installer writes marker_path into the target root, polled over the guest agent.
	// NEVER a sleep.
	Done string `yaml:"done,omitempty" json:"done,omitempty"`

	MarkerPath string `yaml:"marker_path,omitempty" json:"marker_path,omitempty"`
}

#DistroInstaller — the UNATTENDED-INSTALL renderer vocabulary for a `source.kind: iso` VM. It is to that arm exactly what #Init.service_schema is to `service:`: THIS DISTRO owns the answer-file FORMAT, while the vm entity's `source.installer:` owns the DATA (#VmInstaller). Nothing may infer an installer format from a URL, an ISO name or a base_user — the same rule distro_vocab.cue's header states for every other distro trait, and for the same reason.

Five real formats live behind this one shape: archinstall JSON (Omarchy, Arch), kickstart (Fedora/RHEL), preseed (Debian), Subiquity autoinstall (Ubuntu) and AutoYaST (SUSE). Each is keyed by distro and by nothing else.

type DistroInstallerFile added in v0.2026241.2320

type DistroInstallerFile struct {
	// path is relative to the volume root.
	Path string `yaml:"path,omitempty" json:"path"`

	// content is a Go text/template rendered against #InstallerSeedContext.
	Content string `yaml:"content,omitempty" json:"content"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	// when is a Go-template guard: the file is emitted only when it renders non-empty
	// and not "false". This is what lets ONE vocabulary express a whole optional
	// matrix — an ssh key file only when keys were given, a "defer provisioning"
	// sentinel INSTEAD of credentials, an encryption marker only when encrypting —
	// with no Go branch anywhere.
	When string `yaml:"when,omitempty" json:"when,omitempty"`
}

#DistroInstallerFile is ONE file placed on the answers volume.

type DistroPackages

type DistroPackages struct {
	Package []PackageItem `yaml:"package,omitempty" json:"package,omitempty"`

	Copr []string `yaml:"copr,omitempty" json:"copr,omitempty"`

	Repo []RepoBlock `yaml:"repo,omitempty" json:"repo,omitempty"`

	Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`

	Options []string `yaml:"option,omitempty" json:"option,omitempty"`

	Module []string `yaml:"module,omitempty" json:"module,omitempty"`

	AUR *AUR `yaml:"aur,omitempty" json:"aur,omitempty"`
}

type DistroResolveInput

type DistroResolveInput struct {
	Distro RawBody `yaml:"distro,omitempty" json:"distro"`
}

#DistroResolveInput carries one opaque distro body to project.

type DistroResolveReply

type DistroResolveReply struct {
	Resolved *ResolvedDistro `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#DistroResolveReply wraps the resolved distro.

type Dnf

type Dnf struct {
	MaxParallelDownloads int64 `yaml:"max_parallel_downloads,omitempty" json:"max_parallel_downloads,omitempty"`

	Fastestmirror bool `yaml:"fastestmirror,omitempty" json:"fastestmirror,omitempty"`
}

type DoMode

type DoMode string

DoMode is the act/assert/instruct axis. act = perform a side-effect; assert = run the matchers (read-only); instruct = hand free-form text to the agent grader.

const (
	DoAct      DoMode = "act"
	DoAssert   DoMode = "assert"
	DoInstruct DoMode = "instruct"
)

func StepDoMode

func StepDoMode(s *Step) DoMode

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

type DocParser

type DocParser interface {
	ParseDoc(doc *yaml.Node, t Threaded) (directives map[string]*yaml.Node, pp ParsedProject, err error)
}

DocParser is the swappable per-document PARSE seam: the loader plugin candy implements it (candy/plugin-loader, delegating to loaderkit.ParseDoc), and the host resolves the registered loader provider to it and calls it for every config document — so an alternative loader plugin serves a different config front-end by implementing this. Typed (no wire envelope) since it runs on every document load. `directives` is the reserved-directive mapping (import/discover/version/ repo/defaults/provides); `pp` is the decomposed entity nodes.

type DocShape

type DocShape int

DocShape classifies one parsed YAML document's top level.

const (
	// DocShapeEmpty — a scalar-null / empty mapping document (nothing to load).
	DocShapeEmpty DocShape = iota
	// DocShapeNode — the unified name-first node-form: reserved document
	// directives (version/import/discover/defaults/repo/provides) plus a flat
	// map of arbitrary-name entity nodes (each `<name>: {<discriminator>: …}`),
	// and NO top-level kind-map key. The ONE authoring surface; a legacy
	// kind-keyed / root-shape document is hard-rejected downstream (the host's
	// #NodeDoc CUE gate), not here.
	DocShapeNode
)

func ClassifyDoc

func ClassifyDoc(node *yaml.Node) (DocShape, error)

ClassifyDoc inspects a document's top level and returns its shape: a non-empty mapping is a node-form document (arbitrary entity-name nodes and/or the reserved directives version/import/discover/…); a scalar-null / empty mapping is DocShapeEmpty; a non-mapping top level is an error. Entity + directive validation happens downstream (the loader parse + the #NodeDoc CUE gate).

type Duration

type Duration = string

--- named-pattern scalars (string) ---

type EmitOpts

type EmitOpts struct {
	DryRun               bool
	FormatJSON           bool // print IR as JSON on stdout instead of table
	AllowRepoChanges     bool
	AllowRootTasks       bool
	WithServices         bool
	SkipIncompatible     bool
	AssumeYes            bool // skip sudo preflight, confirmation prompts
	Verify               bool // run layer tests after install
	Pull                 bool // force re-fetch of remote refs / image pull
	BuilderImageOverride string

	// DevLocalPkg marks a DISPOSABLE CHECK BED's deploy — the deploy-side twin of `charly box
	// build --dev-local-pkg`, set by the bed runner and by nothing else. It changes ONE decision:
	// a localpkg candy whose package source cannot be located becomes a hard failure instead of
	// the benign "the candy's curl/COPY task installs it instead" skip. A bed exists to prove the
	// IN-DEVELOPMENT package builds and installs, so a silent skip there makes the bed assert
	// something it never tested — which is exactly what happened when an uninitialized pkg/fedora
	// submodule let a bed skip building the rpm and fail only later, at an unrelated live check.
	DevLocalPkg bool

	// LocalPkgBuild is the dev-local-pkg build context: the in-development binary +
	// plugins + CalVer + arch + charly.yml the executor uses to build the
	// in-development package via the `charly generate-packages` plugin (the
	// `--dev-local-pkg` build path). Populated by the check-bed runner (charly
	// core) when DevLocalPkg is set; nil → the executor discovers defaults
	// (os.Executable(), runtime.GOARCH, `<binary> version`, the baked plugins dir,
	// `<CandyDir>/charly.yml`).
	LocalPkgBuild *LocalPkgBuildContext

	// ParentExec is the DeployExecutor of the parent deployment in a
	// nested tree. Non-nil iff this target is dispatched as a child of
	// another — FleetAddCmd's tree walker builds the chain root-first
	// and passes the immediate ancestor's executor here. Targets that
	// support being nested (host, container, vm) compose their own
	// executor over ParentExec via NestedExecutor; leaf-only targets
	// (kubernetes) ignore it and error if non-nil.
	//
	// When nil, the target runs against its natural root venue
	// (ShellExecutor for host, a fresh SSHExecutor for vm, etc.)
	// — preserving the flat-schema behavior for v2 configs that happen
	// to have no `children:`.
	ParentExec DeployExecutor

	// ParentNode is the FleetNode above this target in the tree.
	// Useful for targets that need parent-level context beyond the
	// executor (e.g. a vm child wants to know its parent container's
	// name to wire network forwarding). nil at the root.
	ParentNode *FleetNode

	// Path is the dotted-path identifier of this node (e.g.
	// "stack.web.db"). Used for logging + ledger keying.
	Path string
}

EmitOpts carries cross-cutting toggles passed by command-line flags. Gates are checked per-step by the target; target-specific options (the container target's registry auth, the host target's --assume-yes, --dry-run) are gathered here too.

func EmitOptsFromResolveTargetAdd added in v0.2026218.138

func EmitOptsFromResolveTargetAdd(req DeployResolveTargetAddRequest, parentExec DeployExecutor) EmitOpts

EmitOptsFromResolveTargetAdd rebuilds the FINAL resolved EmitOpts for the resolve-target-add seam from the plugin-sent gate flags + the re-derived parent executor (K-wave 2 cone R2 bank D thin — the seam's inline construction moved here). ParentExec/Path are the two live/local pieces filled host-side; DryRun never reaches the seam (a dry-run prints plugin-side and returns), so it is absent from the request and left false.

func (EmitOpts) ContextOrDefault

func (o EmitOpts) ContextOrDefault() context.Context

ContextOrDefault returns opts' context if one's attached, or a background context.

type EmitReply

type EmitReply struct {
	Fragment string `yaml:"fragment,omitempty" json:"fragment"`

	ActScript bool `yaml:"act_script,omitempty" json:"act_script,omitempty"`
}

#EmitReply is what a plugin verb/builder returns from an OpEmit Invoke at build time. By default fragment is a verbatim Containerfile FRAGMENT the generator splices into the emitted Containerfile. When act_script is true the fragment is instead a raw state-provision ACT SHELL (the former ProvisionActor.RenderProvisionScript output): the render wraps it in a RUN via the same EmitCmd path a literal command verb uses, rather than splicing it verbatim. This lets a state-provision verb self-declare its build-emit shape over the uniform Invoke(OpEmit), so the render dispatches every verb through InvokeProvider(OpEmit) with no package-main concrete-type assert.

type EmitTarget added in v0.2026232.520

type EmitTarget interface {
	Name() string
	Emit(plans []*InstallPlan, opts EmitOpts) error
}

EmitTarget is the interface OCI + container-deploy + host-deploy emitters satisfy. Taking a slice of plans (rather than a single plan) lets whole-image deploys pass all per-candy plans at once and let the target merge them — useful because OCITarget may want to emit a single Containerfile for the image while the local deploy target may batch steps across candies.

type EncExecInput

type EncExecInput struct {
	Method string `yaml:"method,omitempty" json:"method"`

	ImageID string `yaml:"image_id,omitempty" json:"image_id"`

	BoxName string `yaml:"box_name,omitempty" json:"box_name"`

	Passphrase string `yaml:"passphrase,omitempty" json:"passphrase,omitempty"`

	OldPass string `yaml:"old_pass,omitempty" json:"old_pass,omitempty"`

	NewPass string `yaml:"new_pass,omitempty" json:"new_pass,omitempty"`

	Volumes []EncVolumePlan `yaml:"volumes,omitempty" json:"volumes"`
}

#EncExecInput is the self-contained gocryptfs-execution request the host ships to plugin-enc over OpExecute. ImageID is the systemd-ask-password / extpass id ("charly-<box>"); BoxName is the bare box name for remediation messages; Passphrase drives mount/ensure (gocryptfs init/mount via GOCRYPTFS_PASSWORD); OldPass/NewPass drive passwd. Volumes carries the host-resolved per-volume plan.

type EncExecReply

type EncExecReply struct {
	Error string `yaml:"error,omitempty" json:"error"`
}

#EncExecReply is the execution verdict: Error == "" means success. The host shim turns a non-empty Error into a Go error.

type EncVolumePlan

type EncVolumePlan struct {
	Name string `yaml:"name,omitempty" json:"name"`

	CipherDir string `yaml:"cipher_dir,omitempty" json:"cipher_dir"`

	PlainDir string `yaml:"plain_dir,omitempty" json:"plain_dir"`

	ScopeUnit string `yaml:"scope_unit,omitempty" json:"scope_unit"`

	Initialized bool `yaml:"initialized,omitempty" json:"initialized"`

	Mounted bool `yaml:"mounted,omitempty" json:"mounted"`
}

#EncVolumePlan is one encrypted volume, fully resolved HOST-SIDE: its charly name (for messages), the on-disk cipher/plain dirs, the systemd scope-unit name, and the host-probed initialized/mounted state. The plugin acts on these flags — it never re-derives a charly path convention nor re-probes state.

type EntityRef

type EntityRef = string

--- named-pattern scalars (string) ---

type EnvConfig

type EnvConfig struct {
	Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`
}

#EnvConfig — resolved candy env (KEY=value vars + PATH-append entries). Mirrors kit.EnvConfig.

type EnvDependency

type EnvDependency struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Description string `yaml:"description,omitempty" json:"description"`

	Default string `yaml:"default,omitempty" json:"default,omitempty"`

	Key string `yaml:"key,omitempty" json:"key,omitempty"`
}

EnvDependency (layers.go) — ONE Go type reused for env_require/env_accept, secret_accept/secret_require, AND mcp_require/mcp_accept. The hand struct is a single SUPERSET {name, description, default, key}; the three former CUE defs (#CandyEnvDep/#CandySecretDep/#CandyMCPDep) were distinct only in their per-context name regex (env-var name vs mcp-server name) and the secret-only `key` override — all of which Go validate.go enforces. gengotypes cannot emit THREE shapes for ONE Go type, and a faithful drop-in for []EnvDependency requires every usage to carry all four fields (mcp_require's former 2-field def is missing default/key), so the three collapse to ONE shared #EnvDependency (R3 — one shared abstraction; per-context strictness stays in Go).

type EnvKV

type EnvKV struct {
	Key string `yaml:"key,omitempty" json:"key"`

	Value string `yaml:"value,omitempty" json:"value"`
}

#EnvKV is a deterministic env-var ordering helper (template iteration order). Named #EnvKV, NOT #KeyValue — the schemagen vocab generator auto-registers EVERY top-level `#<X>Value` def as a fake kind-value gate (kindValueDefs, `^#?([A-Za-z0-9]+)Value$`), so `#KeyValue` would spuriously register a "key" kind (RDD-caught live: it appeared in the generated KindValueDefs map). The charly-name alias `KeyValue = EnvKV` (spec/charly_names.go) preserves the exported Go type name every real consumer (deploykit.MapToKeyValueSlice, spec.SortedEnvList) already uses.

type EnvProvideEntry added in v0.2026210.808

type EnvProvideEntry struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Value string `yaml:"value,omitempty" json:"value"`

	Source string `yaml:"source,omitempty" json:"source"`
}

#EnvProvideEntry is a RESOLVED env-provided entry — the raw authored env_provide name→value entry plus the Source deployment it came from. Its GetName/GetSource methods satisfy charly's Named interface structurally (hand-written behavior, not wire shape).

func (EnvProvideEntry) GetName added in v0.2026210.808

func (e EnvProvideEntry) GetName() string

EnvProvideEntry (schema/candy.cue, generated into cue_types_gen.go) is the CUE-sourced RESOLVED env-provided entry — relocated from deploykit (provides.go) so UnifiedFile.Provides (*ProvidesConfig) lives entirely in spec; deploykit keeps the provides PIPELINE (FilterOwnProvides/ResolveTemplate/…), consuming these spec types. Its GetName/GetSource satisfy charly's Named interface — hand-written behavior.

func (EnvProvideEntry) GetSource added in v0.2026210.808

func (e EnvProvideEntry) GetSource() string

type EnvVar

type EnvVar = string

--- named-pattern scalars (string) ---

type EphemeralLifetime

type EphemeralLifetime struct {
	TTL           string `yaml:"ttl,omitempty" json:"ttl,omitempty"`
	KeepOnFailure bool   `yaml:"keep_on_failure,omitempty" json:"keep_on_failure,omitempty"`
	NamingPattern string `yaml:"naming_pattern,omitempty" json:"naming_pattern,omitempty"`
	// contains filtered or unexported fields
}

EphemeralLifetime parameterizes the auto-destruction lifecycle for a deploy.

func (*EphemeralLifetime) EffectiveNamingPattern

func (e *EphemeralLifetime) EffectiveNamingPattern() string

EffectiveNamingPattern returns the configured naming pattern, or the default.

func (*EphemeralLifetime) EffectiveTTL

func (e *EphemeralLifetime) EffectiveTTL() time.Duration

EffectiveTTL returns the configured TTL, or the 1h default.

func (*EphemeralLifetime) IsBoolForm

func (e *EphemeralLifetime) IsBoolForm() bool

IsBoolForm reports whether the field was authored as a bare boolean.

func (*EphemeralLifetime) UnmarshalJSON

func (e *EphemeralLifetime) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the boolean shorthand and the block form (the loader is JSON-based via cue.Value.Decode). `ephemeral: false` is rejected.

type EphemeralRegisterReply

type EphemeralRegisterReply struct {
}

type EphemeralRegisterRequest

type EphemeralRegisterRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Node *Deploy `yaml:"node,omitempty" json:"node"`
}

#EphemeralRegisterRequest/#EphemeralRegisterReply — the host→command:fleet OpEphemeralRegister leg (FINAL/K5 unit 6a): ephemeral_lifecycle.go's cross-substrate ephemeral-instance registration (systemd TTL transient timer + parent-detection + charly.yml persistence) moved to candy/plugin-fleet, the substrate-neutral deploy-lifecycle owner (vm/pod/kubernetes all register through it via deploy_add_shared.go's registerEphemeralIfMarked, which STAYS host-side — candidate-floor sibling of fleet_add_cmd.go — and Invokes this as the FIRST action of every Add). Registration failure is best-effort (logged plugin-side, never fatal to the deploy) — the reply is empty; the host discards the returned handle (the prior in-core contract already did, registerEphemeralIfMarked only checked the error).

type EphemeralRuntime

type EphemeralRuntime struct {
	ID              string `yaml:"id" json:"id"`
	ParentVm        string `yaml:"parent_vm,omitempty" json:"parent_vm,omitempty"`
	ParentSnapshot  string `yaml:"parent_snapshot,omitempty" json:"parent_snapshot,omitempty"`
	ParentEphemeral string `yaml:"parent_ephemeral,omitempty" json:"parent_ephemeral,omitempty"`
	ChildRefcount   int    `yaml:"child_refcount,omitempty" json:"child_refcount,omitempty"`
	TimerUnit       string `yaml:"timer_unit,omitempty" json:"timer_unit,omitempty"`
	TtlDeadline     string `yaml:"ttl_deadline,omitempty" json:"ttl_deadline,omitempty"`
	Status          string `yaml:"status,omitempty" json:"status,omitempty"`
	InstanceName    string `yaml:"instance_name,omitempty" json:"instance_name,omitempty"`
	// DeployAddress is the CLI-addressable deploy identity (the dotted tree path for a nested
	// deploy, e.g. "parent.child" — `charly fleet del <DeployAddress>`), DISTINCT from the
	// dc.Fleet MAP KEY this entry is stored under (which is a dot-SANITIZED "vm:<domain-id>"
	// form — a literal '.' is illegal in a map key, see
	// ValidateDeploymentName (deploy_tree_validate.go); FINAL/K5 unit 6a RCA #2). A nested-ephemeral-child teardown scan
	// (candy/plugin-fleet's teardownChildrenRec) has ONLY this struct to recover the real
	// address from — the map key it was found under cannot be reversed back to the original
	// dotted path (VmDomainIdentity's "." -> "-" replacement is lossy).
	DeployAddress string `yaml:"deploy_address,omitempty" json:"deploy_address,omitempty"`
}

EphemeralRuntime is the live runtime state of an active ephemeral instantiation. Source: charly/deploy.go EphemeralRuntime.

type EphemeralTeardownReply

type EphemeralTeardownReply struct {
}

type EphemeralTeardownRequest

type EphemeralTeardownRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Node *Deploy `yaml:"node,omitempty" json:"node"`
}

#EphemeralTeardownRequest/#EphemeralTeardownReply — the OpEphemeralTeardown leg any substrate's own post-teardown handling can Invoke directly (recursive nested-child teardown, TTL timer cancel, snapshot/parent refcount decrement, charly.yml cleanup lives in candy/plugin-fleet's teardownEphemeral). Today's one caller is candy/plugin-deploy-vm/ lifecycle.go's vmPostTeardown (F6 vm-lifecycle move, coneB-vmlifecycle — formerly a host-side pre-dispatch hook, charly/vm_lifecycle_preresolve.go's vmLifecyclePostTeardown, dispatched in-proc via the now-deleted charly/ephemeral_dispatch.go's TeardownEphemeralLifecycle).

type ExecContext

type ExecContext string

ExecContext is where an op runs. An op's Context list (or its VerbCatalog default) declares legality; the active engine supplies the running context and skips ops whose context set does not include it.

const (
	CtxBuild   ExecContext = "build"
	CtxDeploy  ExecContext = "deploy"
	CtxRuntime ExecContext = "runtime"
)

type ExternalPluginStep added in v0.2026210.2308

type ExternalPluginStep struct {
	Op           *Op      // the run: step op — Plugin (verb word) + PluginInput + RunAs
	CandyName    string   // owning candy (provenance + the ledger CandyRecord key)
	ResolvedUser string   // uid:gid or "root" after resolveUserSpec — drives Scope
	Distros      []string // the image's distro tag chain (img.Tags) for the build-emit BuildEnv
}

ExternalPluginStep is the install-timeline IR node for a `run: plugin: <verb>` step whose verb is served by an EXTERNAL (out-of-process) plugin — a grpcProvider, not a built-in TypedStepProvider (package/service) or ProvisionActor (the state-provision shell verbs) or the `command` install verb. constructOpStep (via the "construct-step" seam's registry consult) routes it here; the Invoke op is picked per venue, placement-agnostic above the registry:

  • the DEPLOY venue (the install runs ON the target, not into an image): BOTH target:local AND target:vm externalized, so an ExternalPluginStep on a deploy is reached by the external local/vm plugin's kit.WalkPlans, which routes it through the host's RunHostStep (the nested ExternalPluginStep case) → executeExternalPluginStep → Invoke(OpExecute) WITH the live DeployExecutor on the go-plugin broker (the E3b reverse channel), so the plugin runs its deploy-context effect (RunSystem/RunUser) on the real venue and RETURNS its teardown ReverseOps which the host records into the ledger (record-and-replay — only recorded ops are reversed at `charly fleet del`). This is the deploy-context counterpart of the build-context OpEmit leg (tasks.go invokeVerbBuildEmit), reusing the SAME spec.DeployReply / ReverseOp wire as the external deploy TARGET (candy/plugin-fleet/deploy_target.go, S3b — was charly core's deploy_target_external.go before the deploy-dispatch cluster moved) — R3.
  • EmitOCI (a BUILD venue — the pod-overlay Containerfile): Invoke(OpEmit) via the SHARED invokeVerbBuildEmit, splicing the plugin's Containerfile FRAGMENT verbatim — it cannot deploy-execute at build (no live venue). Identical behaviour to the box-build path (writeCandySteps → emitTasks `case "plugin"`), just relocated. EmitOCI is the ONLY in-proc Emit* this provider still implements.

Reverse() is static-nil: the reversal ops are DYNAMIC — they come from the plugin's OpExecute reply at emit time, recorded by the host's RunHostStep handler into the CandyRecord (the externalDeployTarget record-and-replay pattern).

func (*ExternalPluginStep) Kind added in v0.2026210.2308

func (s *ExternalPluginStep) Kind() StepKind

func (*ExternalPluginStep) RequiresGate added in v0.2026210.2308

func (s *ExternalPluginStep) RequiresGate() Gate

RequiresGate is GateNone — an external plugin verb-step is operator-authorized build/deploy-time plugin execution (the candy author opted into it), mirroring the external deploy TARGET which is ungated; the plugin picks its own privilege via RunSystem/RunUser on the reverse channel.

func (*ExternalPluginStep) Reverse added in v0.2026210.2308

func (s *ExternalPluginStep) Reverse() []ReverseOp

Reverse is static-nil: the teardown ops are recorded DYNAMICALLY from the plugin's OpExecute DeployReply at emit time (EmitLocal/EmitVM append reply.ReverseOps to the CandyRecord), exactly like the external deploy target — never recomputed.

func (*ExternalPluginStep) Scope added in v0.2026210.2308

func (s *ExternalPluginStep) Scope() Scope

Scope classifies by the step's user context — the SAME rule OpStep uses (OpStepScope): root/empty → system, a named/numeric non-root user → user. The plugin then chooses RunSystem vs RunUser on the reverse channel per its own effect; this scope drives only the ledger reverse-op privilege bookkeeping.

func (*ExternalPluginStep) Venue added in v0.2026210.2308

func (s *ExternalPluginStep) Venue() Venue

type ExternalStep added in v0.2026210.2308

type ExternalStep struct {
	Word       string          // the reserved step word; Kind() = "external:" + Word
	ScopeV     Scope           // plugin-declared (StepContract.scope)
	VenueV     Venue           // plugin-declared (StepContract.venue)
	GateV      Gate            // plugin-declared (StepContract.gate) — the step SKIPs if the gate is not enabled
	Payload    json.RawMessage // opaque per-kind input — the OpExecute params
	CandyName  string          // owning candy (provenance + the ledger CandyRecord key)
	ReverseOps []ReverseOp     // set DYNAMICALLY from the plugin's OpExecute reply (record-and-replay)
}

ExternalStep is an EXTERNAL, plugin-CONTRIBUTED install-step KIND: a step whose Kind() is "external:<word>", carried OPAQUELY (Payload) and whose Scope/Venue/Gate come from the serving class:step plugin's DECLARED StepContract. Its host EXECUTION funnels through the OpExecute-to-the-serving-plugin path; teardown ops are DYNAMIC (recorded from the OpExecute reply), so Reverse() returns the recorded slice.

func (*ExternalStep) Kind added in v0.2026210.2308

func (s *ExternalStep) Kind() StepKind

func (*ExternalStep) RequiresGate added in v0.2026210.2308

func (s *ExternalStep) RequiresGate() Gate

func (*ExternalStep) Reverse added in v0.2026210.2308

func (s *ExternalStep) Reverse() []ReverseOp

func (*ExternalStep) Scope added in v0.2026210.2308

func (s *ExternalStep) Scope() Scope

func (*ExternalStep) Venue added in v0.2026210.2308

func (s *ExternalStep) Venue() Venue

type ExtractStep added in v0.2026224.1942

type ExtractStep struct {
	Source    string // OCI image ref to extract from (e.g. "higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:2.2.1")
	Path      string // path in the source image (a file or a directory)
	Dest      string // absolute destination path on the venue
	CandyName string // owning candy (provenance + the ledger CandyRecord key)
}

ExtractStep materializes a candy's `extract:` entries (FROM <source> AS <candy>-extract-<i> + COPY --from=... in the Containerfile build) onto a MACHINE venue (target:local / target:vm), where the built image filesystem is never used — the install plan executes over the venue's own filesystem. The step runs on the HOST (podman pull → create → cp → rm, charly's own machinery), tars the extracted content, ships it into the venue via PutFile, and extracts it in place — the venue-agnostic analog of the BuilderStep host-engine pattern (RunVenueBuilderStep).

COPY semantics are replicated exactly: when Path is a DIRECTORY its contents are extracted into Dest (Dest is a directory); when Path is a FILE the file lands at Dest (Dest is the file path). Ownership is NOT handled here — the candy's own plan steps chown the extracted paths (the agentteams-higress candy chowns /etc/certs /etc/istio /var/lib/istio /var/log/proxy to uid 1000), mirroring the build where COPY --chown is explicit per entry.

Compiled ONLY for machine venues (hostCtx.MachineVenue): the OCI/pod targets emit the Containerfile extract stages directly and never see this step. Like BuilderStep it is a HOST-ENGINE kind — the walk routes it to RunHostStep, which dispatches it to the class:step plugin's OpExecute with the injected image resolve/ensure closures.

func (*ExtractStep) Kind added in v0.2026224.1942

func (s *ExtractStep) Kind() StepKind

func (*ExtractStep) RequiresGate added in v0.2026224.1942

func (s *ExtractStep) RequiresGate() Gate

func (*ExtractStep) Reverse added in v0.2026224.1942

func (s *ExtractStep) Reverse() []ReverseOp

Reverse returns no ledger ops — the extracted content is the candy's own install surface (a system-level install, like a package), removed (if ever) by the operator, not by deploy teardown. Mirrors LocalPkgInstallStep's empty Reverse: the dest may be a file OR a directory, and the existing rm-file reverse kinds cannot express both (rm -f fails on a dir; the recursive kind runs as the deploy user, who cannot remove system paths).

func (*ExtractStep) Scope added in v0.2026224.1942

func (s *ExtractStep) Scope() Scope

Scope is system — extract lands in system locations (/usr/local/bin, /app, /opt, /var).

func (*ExtractStep) Venue added in v0.2026224.1942

func (s *ExtractStep) Venue() Venue

Venue is host-native — the podman extract runs on the HOST (like the aur builder), then the content is shipped to the target and extracted in place.

type ExtractYAML

type ExtractYAML = CandyExtract

--- candy sub-types ---

type FeatureEntity

type FeatureEntity struct {
	Kind string `yaml:"kind,omitempty" json:"kind"`

	Name string `yaml:"name,omitempty" json:"name"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

#FeatureEntity is one enumerated kind: entity + its RAW plan data (Step is already a plain CUE-sourced wire type, so no separate flattened form is needed on the wire). An entity with neither a description nor a plan is still listed (as "(no description)") but the plugin skips summarizing/validating it, matching the former engine.

type FileStep added in v0.2026210.2308

type FileStep struct {
	Source    string      // path to source content (under layer dir or .build/_inline/)
	Dest      string      // absolute destination path
	Mode      os.FileMode // permissions
	Owner     string      // "root" or "uid:gid" or the invoking user's name
	CandyName string      // for ledger
}

FileStep places a single file. The compiler may emit these for candy-declared file directives that aren't wrapped as tasks (e.g. supervisord fragment assembly). Today's tasks.go handles most file placement via OpStep; FileStep exists for cases where the compiler synthesizes file writes that weren't in the candy manifest (e.g. service unit files, managed-block contents).

func (*FileStep) Kind added in v0.2026210.2308

func (s *FileStep) Kind() StepKind

func (*FileStep) RequiresGate added in v0.2026210.2308

func (s *FileStep) RequiresGate() Gate

func (*FileStep) Reverse added in v0.2026210.2308

func (s *FileStep) Reverse() []ReverseOp

func (*FileStep) Scope added in v0.2026210.2308

func (s *FileStep) Scope() Scope

func (*FileStep) Venue added in v0.2026210.2308

func (s *FileStep) Venue() Venue

type FleetConfig added in v0.2026219.1057

type FleetConfig struct {
	Provides *ProvidesConfig      `yaml:"provides,omitempty" json:"provides,omitempty"`
	Fleet    map[string]FleetNode `yaml:"deploy" json:"deploy"`
	// Sidecar carries the project's sidecar-template library as OPAQUE bodies
	// (the raw PluginKinds["sidecar"] map). candy/plugin-sidecar's OpResolve merges
	// these UNDER each deploy node's own overrides; the kernel reads no fields
	// (the sidecar de-type, Cutover D).
	Sidecar map[string]json.RawMessage `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
	// Cache is the per-host LOCAL cache status (git metadata cache). The unified
	// home for local system state — see spec/loadmodel.go UnifiedFile.Cache.
	Cache *CacheConfig `yaml:"cache,omitempty" json:"cache,omitempty"`
	// Ledger is the per-host INSTALL LEDGER (deploy/candy/step records).
	Ledger *LedgerConfig `yaml:"ledger,omitempty" json:"ledger,omitempty"`
	// System is the per-host LOCAL SYSTEM INFO.
	System *SystemInfo `yaml:"system,omitempty" json:"system,omitempty"`
}

FleetConfig represents per-machine deployment overrides (~/.config/charly/charly.yml). Only runtime/deployment fields are supported — build-time fields are structurally excluded.

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

func (*FleetConfig) Lookup added in v0.2026219.1057

func (dc *FleetConfig) Lookup(deployName, instance string) (FleetNode, bool)

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

func (*FleetConfig) LookupKey added in v0.2026219.1057

func (dc *FleetConfig) LookupKey(key string) (FleetNode, bool)

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

type FleetNode added in v0.2026219.1057

type FleetNode = Deploy

--- top-level entity types ---

type Format

type Format struct {
	CacheMount []CacheMount `yaml:"cache_mount,omitempty" json:"cache_mount,omitempty"`

	SectionFields map[string]string `yaml:"section_field,omitempty" json:"section_field,omitempty"`

	UninstallTemplate string `yaml:"uninstall_template,omitempty" json:"uninstall_template,omitempty"`

	Phases *PhaseSet `yaml:"phase,omitempty" json:"phase,omitempty"`

	Validate []FormatRule `yaml:"validate,omitempty" json:"validate,omitempty"`

	Secondary bool `yaml:"secondary,omitempty" json:"secondary,omitempty"`

	LocalPkg *LocalPkg `yaml:"local_pkg,omitempty" json:"local_pkg,omitempty"`
}

type FormatRule

type FormatRule struct {
	Field string `yaml:"field,omitempty" json:"field"`

	Rule string `yaml:"rule,omitempty" json:"rule"`
}

type Gate

type Gate string

Gate is the name of a CLI flag that must be enabled for a given step to run on the host target. Steps without a gate run unconditionally; steps with a gate are skipped (with a warning) unless the user opts in.

The OCI target ignores gates — container builds are already isolated, so gating would only slow down image construction without adding safety.

const (
	GateNone             Gate = ""
	GateAllowRepoChanges Gate = "allow-repo-changes"
	GateAllowRootTasks   Gate = "allow-root-tasks"
	GateWithServices     Gate = "with-services"
)

type GitCache added in v0.2026241.931

type GitCache struct {
	LatestTags map[string]GitCacheEntry `yaml:"latest_tags,omitempty" json:"latest_tags,omitempty"`

	DefaultBranches map[string]GitCacheEntry `yaml:"default_branches,omitempty" json:"default_branches,omitempty"`

	ResolvedRefs map[string]GitCacheEntry `yaml:"resolved_refs,omitempty" json:"resolved_refs,omitempty"`

	Downloads map[string]GitCacheEntry `yaml:"downloads,omitempty" json:"downloads,omitempty"`
}

#GitCache is the git metadata cache: latest tags, default branches, resolved refs, and repo downloads. Each entry records the value and when it was resolved (RFC3339), so the TTL policy can decide freshness.

type GitCacheEntry added in v0.2026241.931

type GitCacheEntry struct {
	Value string `yaml:"value,omitempty" json:"value"`

	Resolved string `yaml:"resolved,omitempty" json:"resolved"`
}

#GitCacheEntry is one cached git answer: the value plus the resolution time.

type GoogleAccount

type GoogleAccount struct {
	EmailSecret string `yaml:"email_secret,omitempty" json:"email_secret,omitempty"`

	TokenSecret string `yaml:"token_secret,omitempty" json:"token_secret,omitempty"`
}

type GpuProbeInput

type GpuProbeInput struct {
	Action string `yaml:"action,omitempty" json:"action"`

	Group int `yaml:"group,omitempty" json:"group,omitempty"`
}

#GpuProbeInput is the action-multiplexed input to verb:gpu over OpRun. Action selects the host probe. The three data tables (device_patterns/gpu_vendors/pci_class_labels) are now candy/plugin-gpu's OWN embedded data (K5 seam-death — they moved out of charly-core's embedded charly.yml alongside the "hostprobe" HostBuild kind's dissolution; plugin-gpu is the actual detection consumer, so it is the one data source, R3), so this input no longer threads them.

type GpuProbeReply

type GpuProbeReply struct {
	Bool bool `yaml:"bool,omitempty" json:"bool,omitempty"`

	Str string `yaml:"str,omitempty" json:"str,omitempty"`

	Vfio *VFIOReport `yaml:"vfio,omitempty" json:"vfio,omitempty"`

	HostDevices *DetectedDevices `yaml:"host_devices,omitempty" json:"host_devices,omitempty"`

	MemlockSoft uint64 `yaml:"memlock_soft,omitempty" json:"memlock_soft,omitempty"`

	MemlockHard uint64 `yaml:"memlock_hard,omitempty" json:"memlock_hard,omitempty"`
}

#GpuProbeReply is the action-multiplexed reply from verb:gpu. Each action populates only the field(s) it produces.

type GpuSelector

type GpuSelector struct {
	Vendor string `yaml:"vendor,omitempty" json:"vendor"`
}

vendor required + non-empty; NOT regex-pinned (normalizePCIVendor accepts "10DE"/"0X10de"/"0x10de" — a strict regex would reject Go-valid input).

type GpuSwitchInput

type GpuSwitchInput struct {
	Action string `yaml:"action,omitempty" json:"action"`

	Gpu *VFIOGpu `yaml:"gpu,omitempty" json:"gpu,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Addr string `yaml:"addr,omitempty" json:"addr,omitempty"`

	Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty"`
}

#GpuSwitchInput is the action-multiplexed input the core driver-switch shims ship to verb:gpu over OpRun for the DRIVER-SWITCH actions (cutover C9). It rides the SAME verb:gpu provider as the C11 detection actions (GpuProbeInput); the action vocabularies are disjoint, so the plugin's Invoke dispatches by which envelope decodes — detection actions decode GpuProbeInput, switch actions decode this.

type GpuSwitchReply

type GpuSwitchReply struct {
	// "bool" quoted for the same builtin-keyword collision as #GpuProbeReply.
	Bool bool `yaml:"bool,omitempty" json:"bool,omitempty"`

	Str string `yaml:"str,omitempty" json:"str,omitempty"`

	Plan []string `yaml:"plan,omitempty" json:"plan,omitempty"`

	Wedged bool `yaml:"wedged,omitempty" json:"wedged,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

#GpuSwitchReply is the action-multiplexed reply for the DRIVER-SWITCH actions.

type HelmChart added in v0.2026227.1655

type HelmChart struct {
	Repo string `yaml:"repo,omitempty" json:"repo,omitempty"`

	Chart string `yaml:"chart,omitempty" json:"chart"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Release string `yaml:"release,omitempty" json:"release"`

	Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`

	Values_files []string `yaml:"values_files,omitempty" json:"values_files,omitempty"`
}

type HexColor added in v0.2026242.935

type HexColor string

#HexColor is #rrggbb. Six digits only: a renderer that also accepted #rgb or #rrggbbaa would have to normalise, and every consuming format spells alpha differently.

type HolderAddr

type HolderAddr struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Target string `yaml:"target,omitempty" json:"target"`

	Base string `yaml:"base,omitempty" json:"base"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Vm string `yaml:"vm,omitempty" json:"vm,omitempty"`
}

#HolderAddr is the self-contained address of a deployment (holder or claimant) — enough for the host seams to probe/stop/start it WITHOUT re-reading config, so a lease loaded after a crash can act on it. Carries BOTH yaml (the persisted ledger) and json (the Invoke/reverse-channel envelope) tags — the retag step emits both automatically, mirroring the json key.

type HolderDescriptor

type HolderDescriptor struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Holds []string `yaml:"holds,omitempty" json:"holds"`

	Addr HolderAddr `yaml:"addr,omitempty" json:"addr"`

	Restore string `yaml:"restore,omitempty" json:"restore"`
}

#HolderDescriptor is one candidate preemptible holder, pre-projected (PreemptionHolds() + HolderAddrFor() + PreemptEffectiveRestore(), sdk/deploykit's FilterPreemptibleHolders) so the plugin's holdersToStop is pure config-free coordination. Filled plugin-side now (K1-unblock wave 1, off the #ResolvedProject envelope) rather than by a bespoke host reply type.

type Hook added in v0.2026218.1950

type Hook struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Content string `yaml:"content,omitempty" json:"content"`

	Trigger string `yaml:"trigger,omitempty" json:"trigger,omitempty"`

	Matcher string `yaml:"matcher,omitempty" json:"matcher,omitempty"`

	When string `yaml:"when,omitempty" json:"when,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
}

CUE schema for the `hook` KIND — a first-class HARNESS hook entity (the migration of the superproject's .claude/hooks/* gate scripts into candy config). A `hook:` node is a sibling top-level entity in candy/charly-hooks/charly.yml, carrying the script CONTENT inline.

This is the HARNESS hook (the Claude Code PreToolUse/PrePush git-discipline gates), DISTINCT from the candy lifecycle `hook:` field (#CandyHook {post_enable, pre_remove}): the kind word `hook` lives on the kind-word spine while the lifecycle field lives inside a `candy:` body — different namespaces, no parse conflict (the U0 spike exercises a file carrying both).

trigger/matcher absent ⇒ an AUX file (e.g. gitcmd.py, gate_test.py) emitted to .claude/hooks/ but not wired into settings.json. The generator enforces: matcher required iff trigger present.

type HooksConfig

type HooksConfig = CandyHook

--- candy sub-types ---

type HostContext added in v0.2026213.1726

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

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

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

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

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

	// ActiveInitName/ActiveInit carry the MachineVenue's preresolved active init system —
	// resolved ONCE per whole-deploy compile plugin-side off the resolved-project envelope's
	// rp.Init (candy/plugin-fleet/compile.go). compileServiceSteps reads these instead of
	// re-deriving the active init per-candy or guessing via a container-oriented auto-detect
	// heuristic (which cannot disambiguate a machine venue's init from a plain custom-exec
	// service entry — proven live 2026-07-20). Nil/empty for a direct BuildDeployPlan caller /
	// test that compiles outside the seam; compileServiceSteps falls back to its own lazy
	// per-call lookup then. `json:"-"`: IN-PROCESS-ONLY, populated plugin-side — never crosses.
	ActiveInitName string        `json:"-"`
	ActiveInit     *ResolvedInit `json:"-"`
}

host_context.go — the deploy-compile HOST CONTEXT value type, promoted from sdk/deploykit (#55 K4 import-purity). A hand-written spec value carrier, NOT a CUE wire type: it crosses the plugin boundary ONLY as the opaque `host_context: bytes` RawBody payload on #DeployCompileRequest (the sanctioned VmJSON/PodConfigJSON idiom), so CUE never decodes it at the wire. Its WIRE form is the 4 host-computed SCALARS below (MachineVenue/Distro/GlibcVersion/ BuilderImage); the remaining fields (BuilderContext/ActiveInitName/ActiveInit) are `json:"-"` IN-PROCESS-ONLY — populated PLUGIN-SIDE after the HostContextJSON decode (candy/plugin-fleet's preresolveBuilderContexts over the reverse channel + the rp.Init active-init resolve), never crossing. The `cue exp gengotypes` spike is the SDD arbiter: it CANNOT express the `json:"-"` intent (it emits `,omitempty`, which would wrongly serialize the in-process fields into the wire blob) and it embeds the already-hand spec.BuilderPreresolved (opaque map[string]any Context) — so hand-written is SPIKE-JUSTIFIED per the SDD mandate, mirroring its child BuilderPreresolved. sdk/deploykit keeps a `type HostContext = spec.HostContext` forwarder so the deploy-compile mechanism (BuildDeployPlan + the compile helpers) + candy/plugin-fleet compile unchanged.

type HostEnv

type HostEnv struct {
	CharlyBin string `yaml:"charly_bin,omitempty" json:"charly_bin,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	// version is the host charly's CalVer (CharlyVersion()) — the
	// delivery-decision authority for EnsureCharlyInGuest.
	Version string `yaml:"version,omitempty" json:"version,omitempty"`
}

#HostEnv is the generic host identity a lifecycle plugin (running ON the host) needs but cannot derive: the host charly binary path and the host home.

type ImageUserInput

type ImageUserInput struct {
	Ref string `yaml:"ref,omitempty" json:"ref"`

	UID int `yaml:"uid,omitempty" json:"uid"`
}

#ImageUserInput — inspect a remote image's /etc/passwd for the user with the given uid (the build engine's base-image adopt-user probe).

type ImportEntry added in v0.2026210.808

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

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

type ImportList added in v0.2026210.808

type ImportList []ImportEntry

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

func (ImportList) MarshalYAML added in v0.2026210.808

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

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

func (*ImportList) UnmarshalYAML added in v0.2026210.808

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

UnmarshalYAML decodes the mixed-shape import list.

type Incident

type Incident struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id,omitempty"`

	State string `yaml:"state,omitempty" json:"state"`

	Summary string `yaml:"summary,omitempty" json:"summary"`

	CreatedAt string `yaml:"created_at,omitempty" json:"created_at"`

	EvidenceRefs []string `yaml:"evidence_refs,omitempty" json:"evidence_refs,omitempty"`
}

type Init

type Init struct {
	CandyFields []string `yaml:"candy_field,omitempty" json:"candy_field,omitempty"`

	CandyFiles []string `yaml:"candy_file,omitempty" json:"candy_file,omitempty"`

	DependsCandy string `yaml:"depends_candy,omitempty" json:"depends_candy,omitempty"`

	RequiresCapability []string `yaml:"requires_capability,omitempty" json:"requires_capability,omitempty"`

	// The one mandatory field: the build model.
	Model string `yaml:"model,omitempty" json:"model"`

	HeaderFile string `yaml:"header_file,omitempty" json:"header_file,omitempty"`

	FragmentDir string `yaml:"fragment_dir,omitempty" json:"fragment_dir,omitempty"`

	RelayTemplate string `yaml:"relay_template,omitempty" json:"relay_template,omitempty"`

	StageName string `yaml:"stage_name,omitempty" json:"stage_name,omitempty"`

	StageHeaderCopy string `yaml:"stage_header_copy,omitempty" json:"stage_header_copy,omitempty"`

	StageFragmentCopy string `yaml:"stage_fragment_copy,omitempty" json:"stage_fragment_copy,omitempty"`

	AssemblyTemplate string `yaml:"assembly_template,omitempty" json:"assembly_template,omitempty"`

	SystemEnableTemplate string `yaml:"system_enable_template,omitempty" json:"system_enable_template,omitempty"`

	PostAssemblyTemplate string `yaml:"post_assembly_template,omitempty" json:"post_assembly_template,omitempty"`

	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	FallbackEntrypoint []string `yaml:"fallback_entrypoint,omitempty" json:"fallback_entrypoint,omitempty"`

	ManagementTool string `yaml:"management_tool,omitempty" json:"management_tool,omitempty"`

	ManagementCommands map[string]string `yaml:"management_command,omitempty" json:"management_command,omitempty"`

	LabelKey string `yaml:"label_key,omitempty" json:"label_key,omitempty"`

	ServiceSchema *InitServiceSchema `yaml:"service_schema,omitempty" json:"service_schema,omitempty"`
}

type InitConfig added in v0.2026213.1726

type InitConfig struct {
	Init map[string]*ResolvedInit `yaml:"init,omitempty" json:"init"`
}

#InitConfig is the `init:` build-vocabulary container — the whole init map keyed by init-system name (charly/charly.yml's `init:` section). #55 Cluster-B: CUE-sourced (completing #72, which CUE-sourced its siblings #DistroConfig / #Builder but left InitConfig hand-written) so every charly/plugin file that uses ONLY this value type reaches it over spec+proto — the import-purity lever. Its detection / resolution methods (DetectCandyInit / ResolveInitSystem / ActiveInit / InitNames) are pure Go METHODS CUE cannot express and stay hand-written in spec/init_config_methods.go (mirrors #DistroConfig's methods in spec/distro_config_methods.go: a method, not a type). This is NOT a wire type — it is never marshaled across the host↔plugin boundary; it mirrors the authored `init:` config surface, so SDD CUE-sources it exactly as #DistroConfig.

func ProjectInitConfig added in v0.2026213.1726

func ProjectInitConfig(uf *UnifiedFile, resolveInit func(json.RawMessage) (*ResolvedInit, error)) *InitConfig

ProjectInitConfig reconstructs the *InitConfig (init: section) from uf, resolving each opaque `init` body via the caller-supplied resolveInit callback (the init plugin's OpResolve config leg). Nil when no init systems are configured.

func (*InitConfig) ActiveInit added in v0.2026213.1726

func (ic *InitConfig) ActiveInit(layers map[string]CandyReader, candyOrder []string) map[string]*ResolvedInit

ActiveInit returns all init systems that are active for the given image. An image can have multiple active inits (e.g., supervisord + systemd on bootc-flavored compositions).

func (*InitConfig) DetectCandyInit added in v0.2026213.1726

func (ic *InitConfig) DetectCandyInit(ly *CandyYAML, candyPath string) []string

DetectCandyInit returns which init system names a candy triggers, based on its candy manifest fields and file patterns.

func (*InitConfig) InitNames added in v0.2026213.1726

func (ic *InitConfig) InitNames() []string

InitNames returns a sorted list of all init system names.

func (*InitConfig) ResolveInitSystem added in v0.2026213.1726

func (ic *InitConfig) ResolveInitSystem(layers map[string]CandyReader, candyOrder []string, explicit string) (string, *ResolvedInit)

ResolveInitSystem determines the active init system for an image. Priority: explicit override → auto-detect from candies. Returns ("", nil) if no init system is needed.

Candy capability requirements (RequiresCapabilities) are checked against the aggregated candy caps for the composition; init systems whose requirements aren't met are filtered out. The aggregated caps are also consulted for the bootc-prefer-systemd heuristic via PreserveUser (the canonical signal that this is a bootc-flavored composition).

type InitDef

type InitDef = Init

--- top-level entity types ---

type InitResolveInput

type InitResolveInput struct {
	Init RawBody `yaml:"init,omitempty" json:"init"`
}

#InitResolveInput is candy/plugin-init's OpResolve config-leg input: the opaque init body to resolve into a ResolvedInit.

type InitResolveReply

type InitResolveReply struct {
	Resolved *ResolvedInit `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#InitResolveReply wraps the resolved init.

type InitResolveRequest

type InitResolveRequest struct {
	Render *ServiceRenderInput `yaml:"render,omitempty" json:"render,omitempty"`

	Config *InitResolveInput `yaml:"config,omitempty" json:"config,omitempty"`
}

#InitResolveRequest is the discriminated OpResolve input for candy/plugin-init: exactly one of Render (leg 1 — render one service unit) or Config (legs 2–4 — the resolved init envelope) is set.

type InitServiceSchema

type InitServiceSchema struct {
	ServiceTemplate string `yaml:"service_template,omitempty" json:"service_template,omitempty"`

	UnitPathTemplate string `yaml:"unit_path_template,omitempty" json:"unit_path_template,omitempty"`

	DropinTemplate string `yaml:"dropin_template,omitempty" json:"dropin_template,omitempty"`

	DropinPathTemplate string `yaml:"dropin_path_template,omitempty" json:"dropin_path_template,omitempty"`

	SupportsPackaged bool `yaml:"supports_packaged,omitempty" json:"supports_packaged,omitempty"`
}

type InlineCandy added in v0.2026210.808

type InlineCandy struct {
	From      string `yaml:"from,omitempty" json:"from,omitempty"`
	CandyYAML `yaml:",inline"`
	// Manifest carries the discovery manifest filename for a `From:` directory so the candy
	// scan reads the right file. Not YAML-authored; carried through the opaque candy-map fold
	// via JSON, hence exported + json-tagged.
	Manifest string `yaml:"-" json:"__manifest,omitempty"`
}

InlineCandy is a candy declared inline in the unified file's `candy:` map. Mutually exclusive options: `from:` points at a directory to scan (the loader's CandyScanner seam), OR the inline body defines the candy (same fields as the candy manifest, flattened via yaml:",inline").

func DecodeInlineCandy added in v0.2026210.808

func DecodeInlineCandy(raw json.RawMessage) (*InlineCandy, bool)

DecodeInlineCandy decodes one opaque layer body into the InlineCandy loader shape.

type InstallContext

type InstallContext struct {
	CacheMounts []CacheMount `yaml:"cache_mounts,omitempty" json:"cache_mounts,omitempty"`

	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Repos []map[string]any `yaml:"repos,omitempty" json:"repos,omitempty"`

	Options []string `yaml:"options,omitempty" json:"options,omitempty"`

	Copr []string `yaml:"copr,omitempty" json:"copr,omitempty"`

	Modules []string `yaml:"modules,omitempty" json:"modules,omitempty"`

	Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`

	Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"`

	StageName string `yaml:"stage_name,omitempty" json:"stage_name,omitempty"`

	BuilderRef string `yaml:"builder_ref,omitempty" json:"builder_ref,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	UID int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int `yaml:"gid,omitempty" json:"gid,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`
}

#InstallContext — data a distro format's install/prepare/cleanup template renders against: packages + repo/copr/module/exclude/key modifiers, resolved cache mounts, and the builder-stage identity/uid/gid/home.

type InstallOpts

type InstallOpts struct {
	WithServices bool `yaml:"with_service,omitempty" json:"with_service,omitempty"`

	AllowRepoChanges bool `yaml:"allow_repo_changes,omitempty" json:"allow_repo_changes,omitempty"`

	AllowRootTasks bool `yaml:"allow_root_tasks,omitempty" json:"allow_root_tasks,omitempty"`

	SkipIncompatible bool `yaml:"skip_incompatible,omitempty" json:"skip_incompatible,omitempty"`

	Verify bool `yaml:"verify,omitempty" json:"verify,omitempty"`

	BuilderImage string `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`
}

install_opts gates (deploy.go InstallOptsConfig) — shared by local + deploy.

type InstallOptsConfig

type InstallOptsConfig = InstallOpts

--- box / deploy sub-types ---

type InstallPlan

type InstallPlan struct {
	// Identity — populated by the compiler.
	DeployID string // per-deploy unique ID (hash of image + add_candy list)
	Box      string // deployable box name (or candy name for single-candy deploys)
	Version  string // candy/box CalVer version
	Distro   string // resolved host distro tag, e.g. "fedora:43"
	Candy    string // candy name when this plan is for a single candy; "" for whole-image merges

	// The ordered step sequence.
	Steps []InstallStep

	// Provenance — used by teardown and status.
	CandiesIncluded []string          // ordered layer names this plan composes (for whole-image merges)
	AddCandies      []string          // layers added on top via charly.yml add_layers: (for provenance)
	BuilderImage    string            // selected builder image for VenueContainerBuilder steps
	Meta            map[string]string // free-form metadata (builder image, glibc version, …)
}

InstallPlan is the full ordered list of steps for one candy or one whole-image deploy. Compiled by the deploykit compiler and consumed by any EmitTarget implementation.

The compiler produces one InstallPlan per candy (then merges them in topological order for whole-image deploys). A whole-image deploy keeps candy boundaries visible so the ledger can refcount which candies participate.

func PlanFromView added in v0.2026210.2308

func PlanFromView(v InstallPlanView) (*InstallPlan, error)

func PlansFromViews added in v0.2026218.138

func PlansFromViews(viewsJSON json.RawMessage) ([]*InstallPlan, error)

PlanFromView re-materializes the rich in-core *InstallPlan from its JSON-roundtrippable InstallPlanView wire form — the REVERSE of WireView, used by the host to reconstruct []*InstallPlan from the command:fleet plugin's OpCompile reply (K4-B). Steps round-trip through the SINGLE stepsFromView converter (install_step_view.go), already proven round-trip-faithful by TestStepView_RoundTrip. The host re-materialized plan is byte-equivalent to the former in-proc compile output (the K4-B parity golden proves it via DeepEqual against the OLD host-compile path). PlansFromViews decodes a marshalled []InstallPlanView (the wire shape a command plugin ships for already-compiled plans) and re-materializes each into an *InstallPlan — the resolve-target-add seam's per-view loop, consolidated here (K-wave 2 cone R2 bank D thin).

func (*InstallPlan) StepsByVenue

func (p *InstallPlan) StepsByVenue() []StepBatch

StepsByVenue partitions the plan's steps by (Scope, Venue) tuple while preserving intra-partition order. Host target emission uses this to batch contiguous same-(scope, venue) runs into one heredoc. Not used by the OCI target (it walks Steps directly). Uses only the InstallStep interface (Scope()/Venue()), so it belongs with the envelope.

type InstallPlanView

type InstallPlanView struct {
	DeployID string `yaml:"deploy_id,omitempty" json:"deploy_id,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Distro string `yaml:"distro,omitempty" json:"distro,omitempty"`

	Candy string `yaml:"candy,omitempty" json:"candy,omitempty"`

	CandiesIncluded []string `yaml:"candies_included,omitempty" json:"candies_included,omitempty"`

	AddCandies []string `yaml:"add_candies,omitempty" json:"add_candies,omitempty"`

	BuilderImage string `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`

	Meta map[string]string `yaml:"meta,omitempty" json:"meta,omitempty"`

	// steps is the serializable per-step IR — the ordered InstallStep sequence
	// the in-core InstallPlan carries, projected onto the wire union below.
	Steps []InstallStepView `yaml:"steps,omitempty" json:"steps,omitempty"`
}

#InstallPlanView is the JSON-roundtrippable wire VIEW of an InstallPlan.

func WireView added in v0.2026210.2308

func WireView(p *InstallPlan) InstallPlanView

WireView projects the rich in-core InstallPlan onto the JSON-roundtrippable InstallPlanView the host marshals into an external deploy/step provider's op.Params. The Steps interface slice round-trips through the SINGLE StepsToView / stepsFromView converter (install_step_view.go) — an external deploy/step plugin walks the same ordered step IR the in-proc EmitTargets walk and EXECUTES it on the venue (R3; proven by the step-IR round-trip test). The remaining fields are identity + provenance.

A free function (not a method on InstallPlan) because it type-switches the concrete step vocabulary via StepsToView.

type InstallStep

type InstallStep interface {
	// Kind returns the step's concrete type discriminator.
	Kind() StepKind

	// Scope classifies where the effect lands on the target filesystem.
	Scope() Scope

	// Venue classifies where the commands physically execute.
	Venue() Venue

	// RequiresGate names the opt-in flag that must be enabled, or
	// GateNone if the step can run unconditionally. Only consulted by
	// host-target emission; the OCI target ignores gates.
	RequiresGate() Gate

	// Reverse returns the teardown actions this step contributes to the
	// ledger. Called at install time (not at teardown) so the ledger
	// captures the exact reversal actions tied to the specific artifacts
	// created. Empty return value means no reversal is recorded (e.g.
	// phases that leave no state).
	Reverse() []ReverseOp
}

InstallStep is one polymorphic step in an InstallPlan. Every concrete step kind (SystemPackages, Builder, Op, File, …) implements it; an EmitTarget walks a plan's steps and dispatches on the returned discriminators.

func StepFromView added in v0.2026210.2308

func StepFromView(v InstallStepView) (InstallStep, error)

StepFromView reconstructs the concrete InstallStep a view projects. An unknown Kind is a hard error (the wire carried a step kind this charly does not know — a genuine version/contract breach, never silently dropped).

type InstallStepView

type InstallStepView struct {
	Kind string `yaml:"kind,omitempty" json:"kind"`

	// Derived ADVISORY fields (Scope()/Venue()/RequiresGate() results).
	Scope Scope `yaml:"scope,omitempty" json:"scope,omitempty"`

	Venue int `yaml:"venue,omitempty" json:"venue,omitempty"`

	Gate string `yaml:"gate,omitempty" json:"gate,omitempty"`

	// payload is the OPAQUE per-kind input for an EXTERNAL (plugin-contributed)
	// step kind.
	Payload RawBody `yaml:"payload,omitempty" json:"payload,omitempty"`

	// reverse_ops is the step's host-computed teardown ops.
	ReverseOps []ReverseOp `yaml:"reverse_ops,omitempty" json:"reverse_ops,omitempty"`

	// Shared identity / provenance.
	CandyName string `yaml:"candy_name,omitempty" json:"candy_name,omitempty"`

	CandyDir string `yaml:"candy_dir,omitempty" json:"candy_dir,omitempty"`

	// SystemPackagesStep + RepoChangeStep + LocalPkgInstallStep.
	Format string `yaml:"format,omitempty" json:"format,omitempty"`

	// SystemPackagesStep + BuilderStep three-phase tag (int Phase: prepare/install/cleanup).
	Phase int `yaml:"phase,omitempty" json:"phase,omitempty"`

	// SystemPackagesStep.
	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Repos []map[string]any `yaml:"repos,omitempty" json:"repos,omitempty"`

	Options []string `yaml:"options,omitempty" json:"options,omitempty"`

	Copr []string `yaml:"copr,omitempty" json:"copr,omitempty"`

	Modules []string `yaml:"modules,omitempty" json:"modules,omitempty"`

	Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`

	Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"`

	CacheMount []CacheMountView `yaml:"cache_mount,omitempty" json:"cache_mount,omitempty"`

	RawInstallContext map[string]any `yaml:"raw_install_context,omitempty" json:"raw_install_context,omitempty"`

	// BuilderStep.
	Builder string `yaml:"builder,omitempty" json:"builder,omitempty"`

	BuilderImage string `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`

	Artifacts []ArtifactView `yaml:"artifacts,omitempty" json:"artifacts,omitempty"`

	RawStageContext map[string]any `yaml:"raw_stage_context,omitempty" json:"raw_stage_context,omitempty"`

	BuilderDef *BuilderDef `yaml:"builder_def,omitempty" json:"builder_def,omitempty"`

	// local_pkg is shared by BuilderStep (aur) + LocalPkgInstallStep.
	LocalPkg *LocalPkg `yaml:"local_pkg,omitempty" json:"local_pkg,omitempty"`

	// OpStep + ExternalPluginStep (Op + the shared user/ctx/distro fields).
	Op *Op `yaml:"op,omitempty" json:"op,omitempty"`

	CtxPath string `yaml:"ctx_path,omitempty" json:"ctx_path,omitempty"`

	ResolvedUser string `yaml:"resolved_user,omitempty" json:"resolved_user,omitempty"`

	To string `yaml:"to,omitempty" json:"to,omitempty"`

	CandyVars map[string]string `yaml:"candy_vars,omitempty" json:"candy_vars,omitempty"`

	Distros []string `yaml:"distros,omitempty" json:"distros,omitempty"`

	// FileStep.
	Source string `yaml:"source,omitempty" json:"source,omitempty"`

	Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`

	Mode uint32 `yaml:"mode,omitempty" json:"mode,omitempty"`

	Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`

	// ServicePackagedStep + ServiceCustomStep.
	Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	TargetScope Scope `yaml:"target_scope,omitempty" json:"target_scope,omitempty"`

	Enable bool `yaml:"enable,omitempty" json:"enable,omitempty"`

	OverridesText string `yaml:"overrides_text,omitempty" json:"overrides_text,omitempty"`

	OverridesPath string `yaml:"overrides_path,omitempty" json:"overrides_path,omitempty"`

	PriorEnabled bool `yaml:"prior_enabled,omitempty" json:"prior_enabled,omitempty"`

	UnitText string `yaml:"unit_text,omitempty" json:"unit_text,omitempty"`

	UnitPath string `yaml:"unit_path,omitempty" json:"unit_path,omitempty"`

	// ShellHookStep.
	EnvVars map[string]string `yaml:"env_vars,omitempty" json:"env_vars,omitempty"`

	PathAdd []string `yaml:"path_add,omitempty" json:"path_add,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	// ShellSnippetStep.
	Origin string `yaml:"origin,omitempty" json:"origin,omitempty"`

	Shell string `yaml:"shell,omitempty" json:"shell,omitempty"`

	Snippet string `yaml:"snippet,omitempty" json:"snippet,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	Destination string `yaml:"destination,omitempty" json:"destination,omitempty"`

	Marker string `yaml:"marker,omitempty" json:"marker,omitempty"`

	UseDropin bool `yaml:"use_dropin,omitempty" json:"use_dropin,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`

	// RepoChangeStep.
	File string `yaml:"file,omitempty" json:"file,omitempty"`

	Content string `yaml:"content,omitempty" json:"content,omitempty"`

	Checksum string `yaml:"checksum,omitempty" json:"checksum,omitempty"`

	// ApkInstallStep.
	ApkPackages []ApkPackageSpec `yaml:"apk_packages,omitempty" json:"apk_packages,omitempty"`

	// LocalPkgInstallStep — the deploy-time executor downloads the published
	// package from the distro repo (the download leg) instead of building from
	// source, so the IR carries the package reference (name + version) rather
	// than the old source-dir anchors (pkgbuild_ref/project_dir).
	PackageName string `yaml:"package_name,omitempty" json:"package_name,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	// ExtractStep — a candy's `extract:` entry materialized onto a machine venue.
	ExtractSource string `yaml:"extract_source,omitempty" json:"extract_source,omitempty"`

	ExtractPath string `yaml:"extract_path,omitempty" json:"extract_path,omitempty"`

	ExtractDest string `yaml:"extract_dest,omitempty" json:"extract_dest,omitempty"`
}

#InstallStepView is the JSON-roundtrippable wire form of ONE InstallStep — a SUPERSET struct: each kind populates the subset of fields it carries, all optional so the wire stays compact.

func StepToView added in v0.2026210.2308

func StepToView(step InstallStep) InstallStepView

StepToView projects one concrete InstallStep onto its serializable wire view.

func StepsToView added in v0.2026210.2308

func StepsToView(steps []InstallStep) []InstallStepView

StepsToView / stepsFromView convert a whole ordered slice. Order is preserved (the InstallPlan's step ordering is load-bearing — see ResolveHome / StepsByVenue).

type InstallerSeedContext added in v0.2026241.2320

type InstallerSeedContext struct {
	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`

	Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"`

	Locale string `yaml:"locale,omitempty" json:"locale,omitempty"`

	Keyboard string `yaml:"keyboard,omitempty" json:"keyboard,omitempty"`

	Username string `yaml:"username,omitempty" json:"username,omitempty"`

	FullName string `yaml:"full_name,omitempty" json:"full_name,omitempty"`

	Email string `yaml:"email,omitempty" json:"email,omitempty"`

	PasswordHash string `yaml:"password_hash,omitempty" json:"password_hash,omitempty"`

	Disk string `yaml:"disk,omitempty" json:"disk,omitempty"`

	Encrypt bool `yaml:"encrypt,omitempty" json:"encrypt,omitempty"`

	// encryption_password is the LUKS passphrase, and it is plaintext by necessity:
	// the installer needs it to create the volume. It is only ever set when
	// encrypt is true, and it makes the rendered answers volume a secret.
	EncryptionPassword string `yaml:"encryption_password,omitempty" json:"encryption_password,omitempty"`

	SSHAuthorizedKeys []string `yaml:"ssh_authorized_key,omitempty" json:"ssh_authorized_key,omitempty"`

	DeferProvisioning bool `yaml:"defer_provisioning,omitempty" json:"defer_provisioning,omitempty"`

	// disk_size_bytes is the TARGET DISK's size in bytes, from the vm entity's
	// disk_size. It is here because a real installer answer file states partition
	// sizes as absolute numbers, not as "the rest of the disk": archinstall, for one,
	// indexes partition['size'] with no default and has no fill-remaining sentinel, so
	// a template that cannot see the disk size cannot describe a root partition at all.
	//
	// BYTES, not a suffixed string, because the arithmetic happens in the template and
	// a template is the wrong place to parse "40G".
	DiskSizeBytes int64 `yaml:"disk_size_bytes,omitempty" json:"disk_size_bytes,omitempty"`

	// answer carries the vm entity's per-distro extras verbatim.
	Answers map[string]string `yaml:"answer,omitempty" json:"answer,omitempty"`
}

#InstallerSeedContext is what a #DistroInstallerFile.content template renders against. Host-computed. The PLAINTEXT password never appears here: the caller crypt()s it first, so no plaintext reaches a template, a log, or a temp file.

type Iterate

type Iterate struct {
	Agent []string `yaml:"agent,omitempty" json:"agent,omitempty"`

	Sandbox string `yaml:"sandbox,omitempty" json:"sandbox"`

	PlateauIteration int `yaml:"plateau_iteration,omitempty" json:"plateau_iteration,omitempty"`

	Prompt string `yaml:"prompt,omitempty" json:"prompt,omitempty"`

	Note *bool `yaml:"note,omitempty" json:"note,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	MCPEndpoint *string `yaml:"mcp_endpoint,omitempty" json:"mcp_endpoint,omitempty"`
}

func (*Iterate) NotesEnabled

func (i *Iterate) NotesEnabled() bool

NotesEnabled reports whether the iteration NOTES.md memory is on (default on).

type IterateConfig

type IterateConfig = Iterate

--- box / deploy sub-types ---

type KeyValue

type KeyValue = EnvKV

--- wire sub-types renamed to dodge the schemagen kindValueDefs collision (a top-level `#<X>Value` CUE def auto-registers as a kind-value gate; see schema/init.cue's #EnvKV doc comment for the RDD-caught mechanism) ---

func SortedEnvList added in v0.2026213.1726

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

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

type Kubernetes added in v0.2026223.1018

type Kubernetes struct {
	// May be empty (a cluster-policy-only template runs no workload itself).
	Box string `yaml:"box,omitempty" json:"box"`

	Replica *int `yaml:"replica,omitempty" json:"replica,omitempty"`

	Resources *KubernetesResources `yaml:"resources,omitempty" json:"resources,omitempty"`

	Hostnames []KubernetesHostname `yaml:"hostnames,omitempty" json:"hostnames,omitempty"`

	KubeconfigContext string `yaml:"kubeconfig_context,omitempty" json:"kubeconfig_context,omitempty"`

	AdmissionPolicy string `yaml:"admission_policy,omitempty" json:"admission_policy,omitempty"`

	DefaultNamespace string `yaml:"default_namespace,omitempty" json:"default_namespace,omitempty"`

	Storage KubernetesStorage `yaml:"storage,omitempty" json:"storage,omitempty"`

	Ingress KubernetesIngressDefaults `yaml:"ingress,omitempty" json:"ingress,omitempty"`

	GatewayAPI KubernetesGatewayAPI `yaml:"gateway_api,omitempty" json:"gateway_api,omitempty"`

	Secret KubernetesSecretsBackend `yaml:"secret,omitempty" json:"secret,omitempty"`

	ImageDefault KubernetesImagesDefaults `yaml:"image_default,omitempty" json:"image_default,omitempty"`

	PodDefault KubernetesPodDefaults `yaml:"pod_default,omitempty" json:"pod_default,omitempty"`

	Observability KubernetesObservability `yaml:"observability,omitempty" json:"observability,omitempty"`

	NetworkPolicy string `yaml:"network_policy,omitempty" json:"network_policy,omitempty"`

	Defaults KubernetesResourceDefaults `yaml:"defaults,omitempty" json:"defaults,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

type KubernetesDeploy added in v0.2026223.1018

type KubernetesDeploy struct {
	Namespace EntityRef `yaml:"namespace,omitempty" json:"namespace,omitempty"`

	Workload string `yaml:"workload,omitempty" json:"workload,omitempty"`

	Patches []KubernetesPatch `yaml:"patches,omitempty" json:"patches,omitempty"`

	Raw []string `yaml:"raw,omitempty" json:"raw,omitempty"`

	// helm_charts — chart releases the kustomize/helm venues install alongside the
	// generated workload. The k8sgen emitter translates each entry into a kustomize
	// `helmCharts:` overlay entry (chart→name, release→releaseName, values_files→
	// valuesFile); the vm/pod/local venues install the same releases via
	// `step:helm-release`. Field set confirmed by the Unit 1 schema spike.
	Helm_charts []HelmChart `yaml:"helm_charts,omitempty" json:"helm_charts,omitempty"`
}

type KubernetesDeployVenue added in v0.2026223.1018

type KubernetesDeployVenue struct {
	OverlayPath string `yaml:"overlay_path,omitempty" json:"overlay_path"`

	TreeRoot string `yaml:"tree_root,omitempty" json:"tree_root,omitempty"`

	KubeContext string `yaml:"kube_context,omitempty" json:"kube_context,omitempty"`

	DeployName string `yaml:"deploy_name,omitempty" json:"deploy_name,omitempty"`
}

#KubernetesDeployVenue is the preresolved deploy:kubernetes substrate payload the host's kubernetes deploy preresolver produces in DeployVenue.Substrate and the candy/plugin-kube deploy:kubernetes provider decodes.

type KubernetesGatewayAPI added in v0.2026223.1018

type KubernetesGatewayAPI struct {
	Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`

	GatewayClass string `yaml:"gateway_class,omitempty" json:"gateway_class,omitempty"`
}

type KubernetesGenFile added in v0.2026223.1018

type KubernetesGenFile struct {
	RelPath string `yaml:"rel_path,omitempty" json:"rel_path"`

	Doc RawBody `yaml:"doc,omitempty" json:"doc"`

	EgressKind string `yaml:"egress_kind,omitempty" json:"egress_kind"`
}

#KubernetesGenFile is one generated manifest the plugin returns: its RELATIVE path (under OutputDir/DeploymentName, e.g. "base/deployment.yaml"), the manifest as JSON (the host unmarshals it back to a value, egress-validates, and writes it as YAML), and the egress kind that gates it ("k8s_object" or "kustomization").

type KubernetesGenInput added in v0.2026223.1018

type KubernetesGenInput struct {
	DeploymentName string `yaml:"deployment_name,omitempty" json:"deployment_name"`

	Instance string `yaml:"instance,omitempty" json:"instance"`

	ImageRef string `yaml:"image_ref,omitempty" json:"image_ref"`

	Deploy Deploy `yaml:"deploy,omitempty" json:"deploy"`

	// cluster is the decoded kind:kubernetes cluster template. After the kubernetes
	// substrate-value de-type (Cutover K) the KERNEL no longer sets it — it
	// ships the opaque body in ClusterRaw and the plugin decodes ClusterRaw
	// into Cluster before generating, so the kernel never types spec.Kubernetes.
	Cluster Kubernetes `yaml:"cluster,omitempty" json:"cluster,omitempty"`

	ClusterRaw RawBody `yaml:"cluster_raw,omitempty" json:"cluster_raw,omitempty"`

	Ports []string `yaml:"ports,omitempty" json:"ports"`

	UID int `yaml:"uid,omitempty" json:"uid"`

	GID int `yaml:"gid,omitempty" json:"gid"`

	OutputDir string `yaml:"output_dir,omitempty" json:"output_dir"`
}

#KubernetesGenInput is the pure-generation input the caller ships to plugin-k8sgen over OpEmit. Deploy is the deployment node (the former FleetNode = spec.Deploy); Cluster is the kind:kubernetes cluster template (the former KubernetesSpec = spec.Kubernetes); Ports / UID / GID are lifted from the image's OCI-label Capabilities host-side so the plugin needs no access to the package-main BoxMetadata type.

type KubernetesGenReply added in v0.2026223.1018

type KubernetesGenReply struct {
	OverlayRelPath string `yaml:"overlay_rel_path,omitempty" json:"overlay_rel_path"`

	Files []KubernetesGenFile `yaml:"files,omitempty" json:"files"`
}

#KubernetesGenReply is the pure-generation output: the RELATIVE overlay path the host joins onto OutputDir/DeploymentName to form the `kubectl apply -k` argument, and the collected manifest files (base resources + base/overlay kustomizations).

type KubernetesGenerateKustomizeReply added in v0.2026223.1018

type KubernetesGenerateKustomizeReply struct {
	OverlayPath string `yaml:"overlay_path,omitempty" json:"overlay_path"`

	TreeRoot string `yaml:"tree_root,omitempty" json:"tree_root"`
}

type KubernetesGenerateKustomizeRequest added in v0.2026223.1018

type KubernetesGenerateKustomizeRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`

	ImageRef string `yaml:"image_ref,omitempty" json:"image_ref"`

	Node *Deploy `yaml:"node,omitempty" json:"node"`

	CapsJSON RawBody `yaml:"caps,omitempty" json:"caps"`

	ClusterJSON RawBody `yaml:"cluster,omitempty" json:"cluster"`

	OutputDir string `yaml:"output_dir,omitempty" json:"output_dir,omitempty"`
}

#KubernetesGenerateKustomizeRequest/#KubernetesGenerateKustomizeReply — the request/reply shape candy/plugin-kube's materializeKustomize (materialize.go) takes/returns. The former Kustomize-generate HostBuild seam this type pair used to cross (FINAL/K5 unit 6a) is RETIRED (K5-A item 6): the egress-validated Kustomize GENERATION now runs ENTIRELY plugin-side — verb:k8sgen + verb:egress reached peer-to-peer via InvokeProvider, disk I/O done directly by the plugin, no host round trip left — so this pair now travels as a plain Go function signature (materializeKustomize's params/return), not a wire envelope. Both callers (candy/plugin-kube/preresolve.go's deploy:kubernetes preresolve, which self-loads the cluster template + image ref/capabilities itself now too — K-wave W3a A3-phase-2 — and candy/plugin-fleet/deploy_from_box.go's source-less from-box path) construct it directly. Cluster/Capabilities ride opaque (the established RawBody idiom this file uses throughout for hand-written host-side types with no CUE def — e.g. CapsJSON/ClusterJSON in this very def below).

type KubernetesHostname added in v0.2026223.1018

type KubernetesHostname struct {
	Host string `yaml:"host,omitempty" json:"host"`

	TLS bool `yaml:"tls,omitempty" json:"tls,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`
}

type KubernetesImagesDefaults added in v0.2026223.1018

type KubernetesImagesDefaults struct {
	PullPolicy string `yaml:"pull_policy,omitempty" json:"pull_policy,omitempty"`

	PullSecrets []string `yaml:"pull_secrets,omitempty" json:"pull_secrets,omitempty"`
}

type KubernetesIngressDefaults added in v0.2026223.1018

type KubernetesIngressDefaults struct {
	Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`

	Class string `yaml:"class,omitempty" json:"class,omitempty"`

	CertIssuer string `yaml:"cert_issuer,omitempty" json:"cert_issuer,omitempty"`

	PathTypeDefault string `yaml:"path_type_default,omitempty" json:"path_type_default,omitempty"`
}

type KubernetesObservability added in v0.2026223.1018

type KubernetesObservability struct {
	ServiceMonitor bool `yaml:"service_monitor,omitempty" json:"service_monitor,omitempty"`

	ServiceMonitorInterval string `yaml:"service_monitor_interval,omitempty" json:"service_monitor_interval,omitempty"`
}

type KubernetesPatch added in v0.2026223.1018

type KubernetesPatch struct {
	Target struct {
		Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

		Name string `yaml:"name,omitempty" json:"name,omitempty"`

		Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`
	} `yaml:"target,omitempty" json:"target"`

	Patch string `yaml:"patch,omitempty" json:"patch"`
}

type KubernetesPodDefaults added in v0.2026223.1018

type KubernetesPodDefaults struct {
	PriorityClass string `yaml:"priority_class,omitempty" json:"priority_class,omitempty"`

	// Raw Kubernetes Toleration objects (Go []map[string]any) — genuine
	// passthrough, so each element stays OPEN.
	Tolerations []map[string]any `yaml:"tolerations,omitempty" json:"tolerations,omitempty"`

	NodeSelector map[string]string `yaml:"node_selector,omitempty" json:"node_selector,omitempty"`
}

type KubernetesResolveInput added in v0.2026223.1018

type KubernetesResolveInput struct {
	Kubernetes RawBody `yaml:"kubernetes,omitempty" json:"kubernetes"`
}

#KubernetesResolveInput carries one opaque kubernetes cluster template body to project.

type KubernetesResolveReply added in v0.2026223.1018

type KubernetesResolveReply struct {
	Resolved *ResolvedKubernetes `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#KubernetesResolveReply wraps the resolved kubernetes cluster template.

type KubernetesResourceDefaults added in v0.2026223.1018

type KubernetesResourceDefaults struct {
	Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`

	Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
}

type KubernetesResourceValues added in v0.2026223.1018

type KubernetesResourceValues struct {
	CPU string `yaml:"cpu,omitempty" json:"cpu,omitempty"`

	Memory string `yaml:"memory,omitempty" json:"memory,omitempty"`
}

type KubernetesResources added in v0.2026223.1018

type KubernetesResources struct {
	Requests KubernetesResourceValues `yaml:"requests,omitempty" json:"requests,omitempty"`

	Limits KubernetesResourceValues `yaml:"limits,omitempty" json:"limits,omitempty"`
}

type KubernetesSecretsBackend added in v0.2026223.1018

type KubernetesSecretsBackend struct {
	Backend string `yaml:"backend,omitempty" json:"backend,omitempty"`

	Store string `yaml:"store,omitempty" json:"store,omitempty"`

	Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
}

type KubernetesSpec added in v0.2026223.1018

type KubernetesSpec = Kubernetes

--- top-level entity types ---

type KubernetesStorage added in v0.2026223.1018

type KubernetesStorage struct {
	ClassDefault string `yaml:"class_default,omitempty" json:"class_default,omitempty"`

	ClassCheap string `yaml:"class_cheap,omitempty" json:"class_cheap,omitempty"`

	ClassEncrypted string `yaml:"class_encrypted,omitempty" json:"class_encrypted,omitempty"`

	ClassFast string `yaml:"class_fast,omitempty" json:"class_fast,omitempty"`

	AccessModeDefault string `yaml:"access_mode_default,omitempty" json:"access_mode_default,omitempty"`
}

type LabelDataEntry

type LabelDataEntry struct {
	Volume string `yaml:"volume,omitempty" json:"volume"`

	Staging string `yaml:"staging,omitempty" json:"staging"`

	Candy string `yaml:"candy,omitempty" json:"candy"`

	Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
}

#LabelDataEntry — a data mapping in ai.opencharly.data (volume/staging/candy required).

type LabelDescriptionSet

type LabelDescriptionSet struct {
	Candy []LabeledDescription `yaml:"candy,omitempty" json:"candy,omitempty"`

	Box []LabeledDescription `yaml:"box,omitempty" json:"box,omitempty"`

	Deploy []LabeledDescription `yaml:"deploy,omitempty" json:"deploy,omitempty"`
}

#LabelDescriptionSet — the three-section (candy/box/deploy) description manifest (ai.opencharly.description). kit.LabelDescriptionSet aliases this.

func (*LabelDescriptionSet) IsEmpty

func (s *LabelDescriptionSet) IsEmpty() bool

IsEmpty reports whether the set carries no labeled descriptions in any layer.

type LabelRouteEntry

type LabelRouteEntry struct {
	Host string `yaml:"host,omitempty" json:"host"`

	Port int `yaml:"port,omitempty" json:"port"`
}

#LabelRouteEntry — a traefik route in ai.opencharly.route (host+port both required).

type LabelSecretEntry

type LabelSecretEntry struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Target string `yaml:"target,omitempty" json:"target"`

	Env string `yaml:"env,omitempty" json:"env,omitempty"`
}

#LabelSecretEntry — a secret requirement in ai.opencharly.secret (name+target required).

type LabelShellSet

type LabelShellSet struct {
	Candy []ShellEntry `yaml:"candy,omitempty" json:"candy,omitempty"`

	Box []ShellEntry `yaml:"box,omitempty" json:"box,omitempty"`

	Deploy []ShellEntry `yaml:"deploy,omitempty" json:"deploy,omitempty"`
}

#LabelShellSet — the three-section shell-init manifest (ai.opencharly.shell).

type LabelSkillEntry added in v0.2026218.1950

type LabelSkillEntry struct {
	Family string `yaml:"family,omitempty" json:"family"`

	Name string `yaml:"name,omitempty" json:"name"`

	Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Content string `yaml:"content,omitempty" json:"content"`

	References []LabelSkillReference `yaml:"references,omitempty" json:"references,omitempty"`

	Triggers []string `yaml:"triggers,omitempty" json:"triggers,omitempty"`

	Category string `yaml:"category,omitempty" json:"category,omitempty"`
}

#LabelSkillEntry — one skill baked into ai.opencharly.skill (the migration of the vestigial ai.opencharly.skill doc-pointer URL into the actual definitions — an image now carries its composed candies' skills, readable directly via ExtractMetadata / `charly box labels` / `charly fleet from-box`, with no external fetch). family+name+content required (an entry without content is not a readable skill); the JSON label wire form of the authoring #Skill (CollectSkills projects authoring → wire).

type LabelSkillReference added in v0.2026218.1950

type LabelSkillReference struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Content string `yaml:"content,omitempty" json:"content"`
}

type LabelVolumeEntry

type LabelVolumeEntry struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Path string `yaml:"path,omitempty" json:"path"`
}

#LabelVolumeEntry — a volume in the ai.opencharly.volume label (name+path both required).

type LabeledDescription

type LabeledDescription struct {
	Origin string `yaml:"origin,omitempty" json:"origin"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

#LabeledDescription — one origin's plan-shaped self-description (MARSHALED into ai.opencharly.description). origin is REQUIRED (json:"origin", no omitempty). kit aliases it.

type LedgerConfig added in v0.2026241.931

type LedgerConfig struct {
	Deploys map[string]DeployRecord `yaml:"deploys,omitempty" json:"deploys,omitempty"`

	Candies map[string]CandyRecord `yaml:"candies,omitempty" json:"candies,omitempty"`
}

#LedgerConfig is the top-level `ledger:` block. `deploys` maps deploy-id → #DeployRecord; `candies` maps candy name → #CandyRecord.

type LibvirtAudio

type LibvirtAudio struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	ID int `yaml:"id,omitempty" json:"id,omitempty"`
}

type LibvirtCPU

type LibvirtCPU struct {
	// mode is REQUIRED-with-default (renderer default host-passthrough) so the
	// custom⇒model if-guard below can reference it (optional fields error when
	// absent). #LibvirtCPU only instantiates when `cpu:` is present.
	Mode string `yaml:"mode,omitempty" json:"mode"`

	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Check string `yaml:"check,omitempty" json:"check,omitempty"`

	Migratable string `yaml:"migratable,omitempty" json:"migratable,omitempty"`

	Topology *LibvirtCPUTopology `yaml:"topology,omitempty" json:"topology,omitempty"`

	Features []LibvirtCPUFeature `yaml:"features,omitempty" json:"features,omitempty"`

	Cache *LibvirtCPUCache `yaml:"cache,omitempty" json:"cache,omitempty"`

	NUMA []LibvirtNUMACell `yaml:"numa,omitempty" json:"numa,omitempty"`
}

type LibvirtCPUCache

type LibvirtCPUCache struct {
	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Level int `yaml:"level,omitempty" json:"level,omitempty"`
}

type LibvirtCPUFeature

type LibvirtCPUFeature struct {
	Policy string `yaml:"policy,omitempty" json:"policy,omitempty"`

	Name string `yaml:"name,omitempty" json:"name"`
}

type LibvirtCPUTopology

type LibvirtCPUTopology struct {
	Sockets int `yaml:"sockets,omitempty" json:"sockets,omitempty"`

	Dies int `yaml:"dies,omitempty" json:"dies,omitempty"`

	Cores int `yaml:"cores,omitempty" json:"cores,omitempty"`

	Threads int `yaml:"threads,omitempty" json:"threads,omitempty"`
}

type LibvirtCPUTune

type LibvirtCPUTune struct {
	Shares int `yaml:"shares,omitempty" json:"shares,omitempty"`

	Period int `yaml:"period,omitempty" json:"period,omitempty"`

	Quota int `yaml:"quota,omitempty" json:"quota,omitempty"`

	GlobalPeriod int `yaml:"global_period,omitempty" json:"global_period,omitempty"`

	GlobalQuota int `yaml:"global_quota,omitempty" json:"global_quota,omitempty"`

	EmulatorPeriod int `yaml:"emulator_period,omitempty" json:"emulator_period,omitempty"`

	EmulatorQuota int `yaml:"emulator_quota,omitempty" json:"emulator_quota,omitempty"`

	IOThreadPeriod int `yaml:"iothread_period,omitempty" json:"iothread_period,omitempty"`

	IOThreadQuota int `yaml:"iothread_quota,omitempty" json:"iothread_quota,omitempty"`

	VCPUPin []LibvirtVCPUPin `yaml:"vcpupin,omitempty" json:"vcpupin,omitempty"`

	EmulatorPin *LibvirtEmulatorPin `yaml:"emulatorpin,omitempty" json:"emulatorpin,omitempty"`

	IOThreadPin []LibvirtIOThreadPin `yaml:"iothreadpin,omitempty" json:"iothreadpin,omitempty"`
}

type LibvirtChannel

type LibvirtChannel struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`

	Source string `yaml:"source,omitempty" json:"source,omitempty"`
}

type LibvirtClock

type LibvirtClock struct {
	Offset string `yaml:"offset,omitempty" json:"offset,omitempty"`

	Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"`

	Adjustment string `yaml:"adjustment,omitempty" json:"adjustment,omitempty"`

	Basis string `yaml:"basis,omitempty" json:"basis,omitempty"`

	Timers []LibvirtTimer `yaml:"timers,omitempty" json:"timers,omitempty"`
}

type LibvirtConsole

type LibvirtConsole struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Target map[string]string `yaml:"target,omitempty" json:"target,omitempty"`
}

type LibvirtDevices

type LibvirtDevices struct {
	Emulator string `yaml:"emulator,omitempty" json:"emulator,omitempty"`

	Disks []LibvirtDisk `yaml:"disks,omitempty" json:"disks,omitempty"`

	Interfaces []LibvirtInterface `yaml:"interfaces,omitempty" json:"interfaces,omitempty"`

	Channels []LibvirtChannel `yaml:"channels,omitempty" json:"channels,omitempty"`

	Serial []LibvirtSerial `yaml:"serial,omitempty" json:"serial,omitempty"`

	Console []LibvirtConsole `yaml:"console,omitempty" json:"console,omitempty"`

	Parallel []LibvirtParallel `yaml:"parallel,omitempty" json:"parallel,omitempty"`

	Graphics []LibvirtGraphics `yaml:"graphics,omitempty" json:"graphics,omitempty"`

	Video []LibvirtVideo `yaml:"video,omitempty" json:"video,omitempty"`

	Audio []LibvirtAudio `yaml:"audio,omitempty" json:"audio,omitempty"`

	Sound []LibvirtSound `yaml:"sound,omitempty" json:"sound,omitempty"`

	Inputs []LibvirtInput `yaml:"inputs,omitempty" json:"inputs,omitempty"`

	USB []LibvirtUSB `yaml:"usb,omitempty" json:"usb,omitempty"`

	RedirDev []LibvirtRedirDev `yaml:"redirdev,omitempty" json:"redirdev,omitempty"`

	Hostdevs []LibvirtHostdev `yaml:"hostdevs,omitempty" json:"hostdevs,omitempty"`

	Filesystems []LibvirtFilesystem `yaml:"filesystems,omitempty" json:"filesystems,omitempty"`

	RNG []LibvirtRNG `yaml:"rng,omitempty" json:"rng,omitempty"`

	TPM []LibvirtTPM `yaml:"tpm,omitempty" json:"tpm,omitempty"`

	Watchdog []LibvirtWatchdog `yaml:"watchdog,omitempty" json:"watchdog,omitempty"`

	MemBalloon *LibvirtMemBalloon `yaml:"memballoon,omitempty" json:"memballoon,omitempty"`

	Shmem []LibvirtShmem `yaml:"shmem,omitempty" json:"shmem,omitempty"`

	IOMMU *LibvirtIOMMU `yaml:"iommu,omitempty" json:"iommu,omitempty"`

	Vsock *LibvirtVsock `yaml:"vsock,omitempty" json:"vsock,omitempty"`

	Panic []LibvirtPanic `yaml:"panic,omitempty" json:"panic,omitempty"`

	Smartcard []LibvirtSmartcard `yaml:"smartcard,omitempty" json:"smartcard,omitempty"`

	Hub []LibvirtHub `yaml:"hub,omitempty" json:"hub,omitempty"`
}

type LibvirtDisk

type LibvirtDisk struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Device string `yaml:"device,omitempty" json:"device,omitempty"`

	Source map[string]string `yaml:"source,omitempty" json:"source,omitempty"`

	Target map[string]string `yaml:"target,omitempty" json:"target,omitempty"`

	Driver map[string]string `yaml:"driver,omitempty" json:"driver,omitempty"`

	Readonly *bool `yaml:"readonly,omitempty" json:"readonly,omitempty"`

	Serial string `yaml:"serial,omitempty" json:"serial,omitempty"`

	WWN string `yaml:"wwn,omitempty" json:"wwn,omitempty"`

	Boot int `yaml:"boot,omitempty" json:"boot,omitempty"`
}

type LibvirtDomain

type LibvirtDomain struct {
	Snippets []string `yaml:"snippets,omitempty" json:"snippets,omitempty"`

	XMLPassthrough string `yaml:"xml_passthrough,omitempty" json:"xml_passthrough,omitempty"`

	Features *LibvirtFeatures `yaml:"features,omitempty" json:"features,omitempty"`

	CPU *LibvirtCPU `yaml:"cpu,omitempty" json:"cpu,omitempty"`

	Clock *LibvirtClock `yaml:"clock,omitempty" json:"clock,omitempty"`

	MemoryBacking *LibvirtMemoryBacking `yaml:"memory_backing,omitempty" json:"memory_backing,omitempty"`

	MemTune *LibvirtMemTune `yaml:"memtune,omitempty" json:"memtune,omitempty"`

	NUMATune *LibvirtNUMATune `yaml:"numatune,omitempty" json:"numatune,omitempty"`

	CPUTune *LibvirtCPUTune `yaml:"cputune,omitempty" json:"cputune,omitempty"`

	IOThreads int `yaml:"iothreads,omitempty" json:"iothreads,omitempty"`

	Devices *LibvirtDevices `yaml:"devices,omitempty" json:"devices,omitempty"`

	SecLabel *LibvirtSecLabel `yaml:"seclabel,omitempty" json:"seclabel,omitempty"`

	LaunchSecurity *LibvirtLaunchSecurity `yaml:"launch_security,omitempty" json:"launch_security,omitempty"`

	Resource *LibvirtResource `yaml:"resource,omitempty" json:"resource,omitempty"`

	SysInfo *LibvirtSysInfo `yaml:"sysinfo,omitempty" json:"sysinfo,omitempty"`

	// qemu_override: QEMU frontend device properties libvirt models no element
	// for, rendered as
	//
	//	<qemu:override><qemu:device alias='ua-…'><qemu:frontend><qemu:property …/>
	//
	// on the libvirt backend and as `-device <dev>,<prop>=<val>` on the qemu one.
	//
	// This is the ONE escape hatch for the device-property axis, deliberately
	// shaped like libvirt's own override element rather than as a field per knob:
	// virtio-gpu alone carries drm_native_context, hostmem, max_hostmem and venus,
	// and rutabaga adds more. A new knob is a YAML line, never a schema change.
	//
	// Keyed by the device's USER alias, which the device itself must declare (e.g.
	// #LibvirtVideo.alias) — an override naming an alias no device carries is a
	// hard render error, not a silent no-op. The `ua-` prefix libvirt demands is
	// enforced on the ALIAS field, so it reaches these keys transitively: a key
	// that is not `ua-`-prefixed can never equal a valid alias, and the renderer
	// then names it as unmatched. (The key is typed `[string]` rather than a
	// pattern: `cue exp gengotypes` renders a pattern-constrained key as an EMPTY
	// Go struct, which would drop every override on the floor.)
	//
	// Requires libvirt >= 8.2.0. An override TAINTS the domain (libvirt records
	// taint flag `custom-device`); that is libvirt's own policy, not charly's.
	QemuOverride map[string]map[string]any `yaml:"qemu_override,omitempty" json:"qemu_override,omitempty"`
}

--------------------------------------------------------------------------- libvirt: LibvirtDomain (libvirt_yaml.go). CLOSED, every sub-type modeled as a #Libvirt<Name> def. Genuine passthroughs stay typed (NOT blanket `{...}`): snippets/xml_passthrough (raw XML) and every map[string]string Go field as `{[string]: string}`. ---------------------------------------------------------------------------

type LibvirtEmulatorPin

type LibvirtEmulatorPin struct {
	CPUSet string `yaml:"cpuset,omitempty" json:"cpuset"`
}

type LibvirtFeatures

type LibvirtFeatures struct {
	ACPI *bool `yaml:"acpi,omitempty" json:"acpi,omitempty"`

	APIC *bool `yaml:"apic,omitempty" json:"apic,omitempty"`

	PAE *bool `yaml:"pae,omitempty" json:"pae,omitempty"`

	SMM *bool `yaml:"smm,omitempty" json:"smm,omitempty"`

	HAP *bool `yaml:"hap,omitempty" json:"hap,omitempty"`

	VMPort *bool `yaml:"vmport,omitempty" json:"vmport,omitempty"`

	PMU *bool `yaml:"pmu,omitempty" json:"pmu,omitempty"`

	HyperV *LibvirtHyperV `yaml:"hyperv,omitempty" json:"hyperv,omitempty"`

	KVM *LibvirtKVM `yaml:"kvm,omitempty" json:"kvm,omitempty"`

	IBS string `yaml:"ibs,omitempty" json:"ibs,omitempty"`
}

type LibvirtFilesystem

type LibvirtFilesystem struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`

	AccessMode string `yaml:"accessmode,omitempty" json:"accessmode,omitempty"`

	Source string `yaml:"source,omitempty" json:"source"`

	Target string `yaml:"target,omitempty" json:"target"`

	Readonly *bool `yaml:"readonly,omitempty" json:"readonly,omitempty"`

	Binary map[string]string `yaml:"binary,omitempty" json:"binary,omitempty"`
}

type LibvirtGraphics

type LibvirtGraphics struct {
	Type string `yaml:"type,omitempty" json:"type"`

	Port int `yaml:"port,omitempty" json:"port,omitempty"`

	AutoPort string `yaml:"autoport,omitempty" json:"autoport,omitempty"`

	Listen LibvirtGraphicsListeners `yaml:"listen,omitempty" json:"listen,omitempty"`

	Passwd string `yaml:"passwd,omitempty" json:"passwd,omitempty"`

	Keymap string `yaml:"keymap,omitempty" json:"keymap,omitempty"`

	// gl: libvirt models <gl> PER GRAPHICS TYPE, not as one shared element, so this
	// is a struct rather than the scalar it used to be. A bare `gl: "yes"` could
	// only ever reach spice's enable= attribute and had NO way to express
	// rendernode= — which is the attribute that points virtio-gpu at a specific
	// host DRM node, and therefore the whole reason a GPU-in-VM candy touches <gl>
	// at all. `<acceleration rendernode=…>` on #LibvirtVideo is NOT the substitute:
	// libvirt documents that one as vhostuser-driver-only.
	GL *LibvirtGraphicsGL `yaml:"gl,omitempty" json:"gl,omitempty"`

	// address/p2p are dbus-only (<graphics type='dbus' address=… p2p=…/>).
	Address string `yaml:"address,omitempty" json:"address,omitempty"`

	// The per-type field rules — <gl> exists only on spice/egl-headless/dbus,
	// gl.enable only on spice/dbus, address/p2p only on dbus — are NOT expressible
	// here. Writing them as `if type == … { gl?: _|_ }` type-checks in CUE but
	// makes `cue exp gengotypes` emit `type LibvirtGraphics any`: it evaluates the
	// definition with `type` still abstract, so every branch stays unresolved, the
	// field kinds reduce to bottom, and the generator degrades the WHOLE struct.
	// That compiles, so the damage is silent — every graphics block would decode
	// into an untyped map. (The `if firmware == …` rules on #Vm are safe only
	// because they TIGHTEN fields to concrete values instead of to bottom.)
	//
	// They are enforced instead as hard render errors in the libvirt bridge's
	// mapGraphics, which is the exact point where the field would otherwise be
	// dropped on the floor, and where the message can name the reason.
	P2P *bool `yaml:"p2p,omitempty" json:"p2p,omitempty"`
}

type LibvirtGraphicsGL added in v0.2026240.1952

type LibvirtGraphicsGL struct {
	Enable *bool `yaml:"enable,omitempty" json:"enable,omitempty"`

	// render_node: absolute path to the host DRM render node (/dev/dri/renderD128).
	// Requires libvirt >= 5.8.0 (spice) / >= 5.10.0 (egl-headless).
	RenderNode string `yaml:"render_node,omitempty" json:"render_node,omitempty"`
}

type LibvirtGraphicsListen

type LibvirtGraphicsListen struct {
	Type    string `yaml:"type,omitempty" json:"type,omitempty"`
	Address string `yaml:"address,omitempty" json:"address,omitempty"`
	Network string `yaml:"network,omitempty" json:"network,omitempty"`
	Socket  string `yaml:"socket,omitempty" json:"socket,omitempty"`
}

--------------------------------------------------------------------------- LibvirtGraphicsListen / LibvirtGraphicsListeners — #LibvirtListen (libvirt_yaml_listen.go). scalar | map | list of listeners. ---------------------------------------------------------------------------

type LibvirtGraphicsListeners

type LibvirtGraphicsListeners []LibvirtGraphicsListen

LibvirtGraphicsListeners is the YAML-shaped list of listeners for one <graphics> element.

func (LibvirtGraphicsListeners) First

First returns the first listener, or the zero value if empty.

func (*LibvirtGraphicsListeners) UnmarshalJSON

func (ll *LibvirtGraphicsListeners) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the three interchangeable listen shapes on the JSON read path (the opaque substrate-decode path a de-typed vm body takes): a bare scalar address ("127.0.0.1" → one type:address listener), a single object, or a list of objects. The YAML read path decodes the same three via cue.Value.Decode running THIS method (the Matcher/PortScope self-decoding strategy), so ONE decoder now serves both paths (R3) — replacing the former charly-side expandLibvirtListenersNode YAML expander.

type LibvirtHostdev

type LibvirtHostdev struct {
	Type    string            `yaml:"type" json:"type"`
	Mode    string            `yaml:"mode,omitempty" json:"mode,omitempty"`
	Managed string            `yaml:"managed,omitempty" json:"managed,omitempty"`
	Source  map[string]string `yaml:"source" json:"source"`
	ROM     map[string]string `yaml:"rom,omitempty" json:"rom,omitempty"`
	Driver  map[string]string `yaml:"driver,omitempty" json:"driver,omitempty"`
}

--------------------------------------------------------------------------- LibvirtHostdev — #LibvirtHostdev (libvirt_yaml.go). The `if type=="pci"` source-redefine degrades the generated def to `any`. ---------------------------------------------------------------------------

type LibvirtHub

type LibvirtHub struct {
	Type string `yaml:"type,omitempty" json:"type"`
}

type LibvirtHugepages

type LibvirtHugepages struct {
	Size string `yaml:"size,omitempty" json:"size,omitempty"`

	NodeSet string `yaml:"nodeset,omitempty" json:"nodeset,omitempty"`
}

type LibvirtHyperV

type LibvirtHyperV struct {
	Relaxed string `yaml:"relaxed,omitempty" json:"relaxed,omitempty"`

	VAPIC string `yaml:"vapic,omitempty" json:"vapic,omitempty"`

	Spinlocks *LibvirtSpinlocks `yaml:"spinlocks,omitempty" json:"spinlocks,omitempty"`

	VPIndex string `yaml:"vpindex,omitempty" json:"vpindex,omitempty"`

	Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"`

	Synic string `yaml:"synic,omitempty" json:"synic,omitempty"`

	STimer string `yaml:"stimer,omitempty" json:"stimer,omitempty"`

	Reset string `yaml:"reset,omitempty" json:"reset,omitempty"`

	VendorID *LibvirtVendorID `yaml:"vendor_id,omitempty" json:"vendor_id,omitempty"`

	Frequencies string `yaml:"frequencies,omitempty" json:"frequencies,omitempty"`

	Reenlightenment string `yaml:"reenlightenment,omitempty" json:"reenlightenment,omitempty"`

	TLBFlush string `yaml:"tlbflush,omitempty" json:"tlbflush,omitempty"`

	IPI string `yaml:"ipi,omitempty" json:"ipi,omitempty"`

	EVMCS string `yaml:"evmcs,omitempty" json:"evmcs,omitempty"`
}

HyperV enlightenment toggles — all "on"/"off"-ish strings; no Go validator, kept plain string to avoid false-rejecting valid libvirt values.

type LibvirtIOMMU

type LibvirtIOMMU struct {
	Model string `yaml:"model,omitempty" json:"model"`

	Driver map[string]string `yaml:"driver,omitempty" json:"driver,omitempty"`
}

type LibvirtIOThreadPin

type LibvirtIOThreadPin struct {
	IOThread int `yaml:"iothread,omitempty" json:"iothread"`

	CPUSet string `yaml:"cpuset,omitempty" json:"cpuset"`
}

type LibvirtInput

type LibvirtInput struct {
	Type string `yaml:"type,omitempty" json:"type"`

	Bus string `yaml:"bus,omitempty" json:"bus,omitempty"`
}

type LibvirtInterface

type LibvirtInterface struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Source map[string]string `yaml:"source,omitempty" json:"source,omitempty"`

	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	MAC string `yaml:"mac,omitempty" json:"mac,omitempty"`

	MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"`

	Driver map[string]string `yaml:"driver,omitempty" json:"driver,omitempty"`

	Boot int `yaml:"boot,omitempty" json:"boot,omitempty"`

	PortForwards []LibvirtPortForward `yaml:"port_forwards,omitempty" json:"port_forwards,omitempty"`
}

type LibvirtKVM

type LibvirtKVM struct {
	Hidden string `yaml:"hidden,omitempty" json:"hidden,omitempty"`

	HintDedicated string `yaml:"hint_dedicated,omitempty" json:"hint_dedicated,omitempty"`

	PollControl string `yaml:"poll_control,omitempty" json:"poll_control,omitempty"`

	PVIPI string `yaml:"pv_ipi,omitempty" json:"pv_ipi,omitempty"`

	DirtyRingSize int `yaml:"dirty_ring_size,omitempty" json:"dirty_ring_size,omitempty"`
}

type LibvirtLaunchSecurity

type LibvirtLaunchSecurity struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	CBitPos int `yaml:"cbitpos,omitempty" json:"cbitpos,omitempty"`

	ReducedPhysBits int `yaml:"reduced_phys_bits,omitempty" json:"reduced_phys_bits,omitempty"`

	Policy string `yaml:"policy,omitempty" json:"policy,omitempty"`

	DhCert string `yaml:"dh_cert,omitempty" json:"dh_cert,omitempty"`

	Session string `yaml:"session,omitempty" json:"session,omitempty"`

	KernelHashes string `yaml:"kernel_hashes,omitempty" json:"kernel_hashes,omitempty"`
}

type LibvirtListenOne

type LibvirtListenOne struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Address string `yaml:"address,omitempty" json:"address,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	Socket string `yaml:"socket,omitempty" json:"socket,omitempty"`
}

type LibvirtMemBalloon

type LibvirtMemBalloon struct {
	Model string `yaml:"model,omitempty" json:"model"`

	Autodeflate string `yaml:"autodeflate,omitempty" json:"autodeflate,omitempty"`

	Stats map[string]int64 `yaml:"stats,omitempty" json:"stats,omitempty"`
}

type LibvirtMemTune

type LibvirtMemTune struct {
	HardLimit string `yaml:"hard_limit,omitempty" json:"hard_limit,omitempty"`

	SoftLimit string `yaml:"soft_limit,omitempty" json:"soft_limit,omitempty"`

	SwapHardLimit string `yaml:"swap_hard_limit,omitempty" json:"swap_hard_limit,omitempty"`

	MinGuarantee string `yaml:"min_guarantee,omitempty" json:"min_guarantee,omitempty"`
}

type LibvirtMemnode

type LibvirtMemnode struct {
	CellID int `yaml:"cellid,omitempty" json:"cellid,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Nodeset string `yaml:"nodeset,omitempty" json:"nodeset,omitempty"`
}

type LibvirtMemoryBacking

type LibvirtMemoryBacking struct {
	Hugepages *LibvirtHugepages `yaml:"hugepages,omitempty" json:"hugepages,omitempty"`

	NoSharepages *bool `yaml:"nosharepages,omitempty" json:"nosharepages,omitempty"`

	Locked *bool `yaml:"locked,omitempty" json:"locked,omitempty"`

	Source string `yaml:"source,omitempty" json:"source,omitempty"`

	Access string `yaml:"access,omitempty" json:"access,omitempty"`

	Allocation string `yaml:"allocation,omitempty" json:"allocation,omitempty"`

	Discard *bool `yaml:"discard,omitempty" json:"discard,omitempty"`
}

type LibvirtNUMACell

type LibvirtNUMACell struct {
	ID int `yaml:"id,omitempty" json:"id,omitempty"`

	CPUs string `yaml:"cpus,omitempty" json:"cpus,omitempty"`

	Memory string `yaml:"memory,omitempty" json:"memory,omitempty"`

	Unit string `yaml:"unit,omitempty" json:"unit,omitempty"`

	MemAccess string `yaml:"memaccess,omitempty" json:"memaccess,omitempty"`
}

type LibvirtNUMAMemory

type LibvirtNUMAMemory struct {
	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Nodeset string `yaml:"nodeset,omitempty" json:"nodeset,omitempty"`

	Placement string `yaml:"placement,omitempty" json:"placement,omitempty"`
}

type LibvirtNUMATune

type LibvirtNUMATune struct {
	Memory *LibvirtNUMAMemory `yaml:"memory,omitempty" json:"memory,omitempty"`

	MemNodes []LibvirtMemnode `yaml:"memnodes,omitempty" json:"memnodes,omitempty"`
}

type LibvirtPCIHex

type LibvirtPCIHex string

PCI source address component: 0x-hex OR bare decimal (hexUintPtr accepts both).

type LibvirtPanic

type LibvirtPanic struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Address map[string]string `yaml:"address,omitempty" json:"address,omitempty"`
}

type LibvirtParallel

type LibvirtParallel struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Source map[string]string `yaml:"source,omitempty" json:"source,omitempty"`

	Target map[string]string `yaml:"target,omitempty" json:"target,omitempty"`
}

type LibvirtPortForward

type LibvirtPortForward struct {
	Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`

	Start int `yaml:"start,omitempty" json:"start"`

	To int `yaml:"to,omitempty" json:"to,omitempty"`
}

type LibvirtRNG

type LibvirtRNG struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Backend string `yaml:"backend,omitempty" json:"backend,omitempty"`

	Rate map[string]string `yaml:"rate,omitempty" json:"rate,omitempty"`
}

type LibvirtRedirDev

type LibvirtRedirDev struct {
	Bus string `yaml:"bus,omitempty" json:"bus,omitempty"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`
}

type LibvirtResource

type LibvirtResource struct {
	Partition string `yaml:"partition,omitempty" json:"partition,omitempty"`

	FibreChannel map[string]string `yaml:"fibrechannel,omitempty" json:"fibrechannel,omitempty"`
}

type LibvirtSecLabel

type LibvirtSecLabel struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Relabel string `yaml:"relabel,omitempty" json:"relabel,omitempty"`

	Label string `yaml:"label,omitempty" json:"label,omitempty"`

	BaseLabel string `yaml:"baselabel,omitempty" json:"baselabel,omitempty"`

	ImageLabel string `yaml:"imagelabel,omitempty" json:"imagelabel,omitempty"`
}

type LibvirtSerial

type LibvirtSerial struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Source map[string]string `yaml:"source,omitempty" json:"source,omitempty"`

	Target map[string]string `yaml:"target,omitempty" json:"target,omitempty"`
}

type LibvirtShmem

type LibvirtShmem struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Role string `yaml:"role,omitempty" json:"role,omitempty"`

	Model map[string]string `yaml:"model,omitempty" json:"model,omitempty"`

	Size string `yaml:"size,omitempty" json:"size,omitempty"`

	Server map[string]string `yaml:"server,omitempty" json:"server,omitempty"`
}

type LibvirtSmartcard

type LibvirtSmartcard struct {
	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`
}

type LibvirtSound

type LibvirtSound struct {
	Model string `yaml:"model,omitempty" json:"model"`
}

type LibvirtSpinlocks

type LibvirtSpinlocks struct {
	State string `yaml:"state,omitempty" json:"state,omitempty"`

	Retries int `yaml:"retries,omitempty" json:"retries,omitempty"`
}

type LibvirtSysInfo

type LibvirtSysInfo struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	BIOS map[string]string `yaml:"bios,omitempty" json:"bios,omitempty"`

	System map[string]string `yaml:"system,omitempty" json:"system,omitempty"`

	BaseBoard []map[string]string `yaml:"baseboard,omitempty" json:"baseboard,omitempty"`

	Chassis map[string]string `yaml:"chassis,omitempty" json:"chassis,omitempty"`

	Processor []map[string]string `yaml:"processor,omitempty" json:"processor,omitempty"`

	OEMStrings []string `yaml:"oem_strings,omitempty" json:"oem_strings,omitempty"`
}

type LibvirtTPM

type LibvirtTPM struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Backend map[string]string `yaml:"backend,omitempty" json:"backend,omitempty"`
}

type LibvirtTimer

type LibvirtTimer struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Present string `yaml:"present,omitempty" json:"present,omitempty"`

	Track string `yaml:"track,omitempty" json:"track,omitempty"`

	TickPolicy string `yaml:"tickpolicy,omitempty" json:"tickpolicy,omitempty"`

	Frequency int `yaml:"frequency,omitempty" json:"frequency,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
}

type LibvirtURI added in v0.2026213.1726

type LibvirtURI struct {
	// Mode is "session" (currently the only supported value).
	Mode string

	// Remote is the SSH target when scheme is qemu+ssh://. Zero
	// value means local (qemu:///session).
	Remote SSHTarget

	// Raw is the original URI string for error messages.
	Raw string
}

LibvirtURI is the parsed form of an `charly --uri …` value.

func ParseLibvirtURI added in v0.2026213.1726

func ParseLibvirtURI(s string) (LibvirtURI, error)

ParseLibvirtURI handles the four recognized shapes. Empty / missing URI returns a local-session value; invalid URIs return an error.

func (LibvirtURI) IsLocal added in v0.2026213.1726

func (u LibvirtURI) IsLocal() bool

IsLocal reports whether the URI targets the local libvirt session. Equivalent to len(u.Remote.Host) == 0.

type LibvirtUSB

type LibvirtUSB struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Port int `yaml:"port,omitempty" json:"port,omitempty"`
}

type LibvirtVCPUPin

type LibvirtVCPUPin struct {
	VCPU int `yaml:"vcpu,omitempty" json:"vcpu"`

	CPUSet string `yaml:"cpuset,omitempty" json:"cpuset"`
}

type LibvirtVendorID

type LibvirtVendorID struct {
	State string `yaml:"state,omitempty" json:"state,omitempty"`

	Value string `yaml:"value,omitempty" json:"value,omitempty"`
}

type LibvirtVideo

type LibvirtVideo struct {
	Model string `yaml:"model,omitempty" json:"model"`

	// device: the concrete QEMU device behind the model. `model` alone cannot select
	// these: model='virtio' emits plain virtio-vga, which has no GL and therefore no
	// blob/native-context support. Requires libvirt >= 12.5.0.
	//
	// The vocabulary is closed because libvirt's own RNG closes it (domaincommon.rng,
	// the type='virtio' group): any other value is rejected at DEFINE time with an
	// error that blames <devices>, not the attribute. Rejecting it here names the field.
	Device string `yaml:"device,omitempty" json:"device,omitempty"`

	Ram int `yaml:"ram,omitempty" json:"ram,omitempty"`

	VRAM int `yaml:"vram,omitempty" json:"vram,omitempty"`

	VRAM64 int `yaml:"vram64,omitempty" json:"vram64,omitempty"`

	VGAMem int `yaml:"vgamem,omitempty" json:"vgamem,omitempty"`

	Heads int `yaml:"heads,omitempty" json:"heads,omitempty"`

	// blob: virtio-gpu blob resources — the guest maps host memory directly
	// instead of copying through the device. Required for a native-context or
	// venus guest. Requires libvirt >= 9.2.0 and QEMU >= 6.1, and the domain MUST
	// have shared memory backing (memory_backing.source: memfd + access: shared).
	Blob *bool `yaml:"blob,omitempty" json:"blob,omitempty"`

	EDID *bool `yaml:"edid,omitempty" json:"edid,omitempty"`

	Accel3D *bool `yaml:"accel3d,omitempty" json:"accel3d,omitempty"`

	Accel2D *bool `yaml:"accel2d,omitempty" json:"accel2d,omitempty"`

	// render_node on <acceleration> is documented by libvirt as VHOSTUSER-DRIVER
	// ONLY (since 5.8.0). To point an ordinary virtio-gpu at a host node, set
	// graphics.gl.render_node instead — that is the attribute libvirt actually
	// reads for it.
	RenderNode string `yaml:"render_node,omitempty" json:"render_node,omitempty"`

	Primary *bool `yaml:"primary,omitempty" json:"primary,omitempty"`

	Resolution *LibvirtVideoResolution `yaml:"resolution,omitempty" json:"resolution,omitempty"`

	Driver *LibvirtVideoDriver `yaml:"driver,omitempty" json:"driver,omitempty"`

	// alias: a libvirt USER alias for this device. Its only purpose is to be
	// targeted by libvirt.qemu_override — libvirt refuses an override against its
	// own auto-assigned alias (video0), which is why the `ua-` prefix is required
	// rather than conventional. Emitted ONLY when declared, so a VM that does not
	// use it renders byte-identically to before this field existed.
	Alias string `yaml:"alias,omitempty" json:"alias,omitempty"`
}

type LibvirtVideoDriver added in v0.2026240.1952

type LibvirtVideoDriver struct {
	// Both enums are closed by libvirt's RNG (domaincommon.rng, the video <driver>
	// element). A name like "qxl" — the plausible guess, since it is a valid video
	// MODEL — is not a valid driver name and fails at define time.
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	VGAConf string `yaml:"vgaconf,omitempty" json:"vgaconf,omitempty"`

	// The virtioOptions toggles. libvirt spells these on/off, where the video model's
	// own attributes beside them are yes/no — the renderer keeps the two apart.
	IOMMU *bool `yaml:"iommu,omitempty" json:"iommu,omitempty"`

	ATS *bool `yaml:"ats,omitempty" json:"ats,omitempty"`

	Packed *bool `yaml:"packed,omitempty" json:"packed,omitempty"`

	PagePerVQ *bool `yaml:"page_per_vq,omitempty" json:"page_per_vq,omitempty"`
}

type LibvirtVideoResolution added in v0.2026240.1952

type LibvirtVideoResolution struct {
	// Both required: libvirt's <resolution> has no default and a 0x0 would be
	// emitted verbatim.
	X int `yaml:"x,omitempty" json:"x"`

	Y int `yaml:"y,omitempty" json:"y"`
}

type LibvirtVsock

type LibvirtVsock struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	CID map[string]string `yaml:"cid,omitempty" json:"cid,omitempty"`
}

type LibvirtWatchdog

type LibvirtWatchdog struct {
	Model string `yaml:"model,omitempty" json:"model"`

	Action string `yaml:"action,omitempty" json:"action,omitempty"`
}

type LifecycleOpts

type LifecycleOpts struct {
	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`

	AllowRepoChanges bool `yaml:"allow_repo_changes,omitempty" json:"allow_repo_changes,omitempty"`

	AllowRootTasks bool `yaml:"allow_root_tasks,omitempty" json:"allow_root_tasks,omitempty"`

	WithServices bool `yaml:"with_services,omitempty" json:"with_services,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	Verify bool `yaml:"verify,omitempty" json:"verify,omitempty"`

	Pull bool `yaml:"pull,omitempty" json:"pull,omitempty"`

	SkipIncompatible bool `yaml:"skip_incompatible,omitempty" json:"skip_incompatible,omitempty"`

	BuilderImageOverride string `yaml:"builder_image_override,omitempty" json:"builder_image_override,omitempty"`
}

#LifecycleOpts is the serializable subset of the host's EmitOpts shipped in a lifecycle Op's params. The two LIVE EmitOpts fields (ParentExec, ParentNode) cannot cross the []byte wire — they re-attach host-side via the reverse channel's live host-build inputs, never serialized. LifecycleOptsFromEmit (spec/deploy_methods.go) is the ONE hand-written converter — a pure function, not a type, so it stays hand-written.

func LifecycleOptsFromEmit

func LifecycleOptsFromEmit(o EmitOpts) LifecycleOpts

LifecycleOptsFromEmit projects an EmitOpts (spec/deploy_executor.go — a LIVE behaviour-contract type that cannot itself cross the wire, since EmitOpts.ParentExec carries a live DeployExecutor) onto its wire-safe LifecycleOpts subset — the ONE shared conversion the deploy-dispatch envelope (S3b, #DeployTargetDispatchRequest.opts_json) uses on BOTH sides: charly core (unified_targets.go's Add/Update) builds this BEFORE marshaling into OptsJSON — never marshal a raw EmitOpts directly — and candy/plugin-fleet decodes OptsJSON directly into a LifecycleOpts, so no plugin-side conversion is needed at all.

type LifecyclePrepareInput

type LifecyclePrepareInput struct {
	Entity string `yaml:"entity,omitempty" json:"entity"`

	VM *ResolvedVm `yaml:"vm,omitempty" json:"vm,omitempty"`

	SSHUser string `yaml:"ssh_user,omitempty" json:"ssh_user"`

	SSHPort int `yaml:"ssh_port,omitempty" json:"ssh_port"`

	Alias string `yaml:"alias,omitempty" json:"alias"`

	SSHKeyPath string `yaml:"ssh_key_path,omitempty" json:"ssh_key_path"`

	KnownHostsPath string `yaml:"known_hosts_path,omitempty" json:"known_hosts_path"`

	StateDir string `yaml:"state_dir,omitempty" json:"state_dir"`

	PriorState *VmDeployState `yaml:"prior_state,omitempty" json:"prior_state,omitempty"`
}

#LifecyclePrepareInput is the host-resolved DATA a vm substrate's OpPrepareVenue needs but cannot derive itself.

type LifecycleTarget added in v0.2026218.138

type LifecycleTarget interface {
	UnifiedDeployTarget

	// Start brings the target up (charly start / podman start / virsh
	// start / systemctl start as appropriate). Idempotent: no-op if
	// already running.
	Start(ctx context.Context) error

	// Stop brings the target down. Idempotent.
	Stop(ctx context.Context) error

	// Status reports the target's live runtime state.
	Status(ctx context.Context) (DeployTargetStatus, error)

	// Logs streams or tails the target's logs. See DeployTargetLogsOpts
	// for follow/tail semantics.
	Logs(ctx context.Context, opts DeployTargetLogsOpts) error

	// Shell opens an interactive shell in the target. cmd, if
	// non-empty, is run instead of starting a login shell.
	Shell(ctx context.Context, cmd []string) error

	// Attach runs an INTERACTIVE or live-stdio session on the target's venue,
	// wired to the operator's terminal. It is the `charly shell` / `charly
	// cmd` leg — distinct from Shell (the `charly service` NON-interactive
	// capture leg). tty selects the resolver + PTY policy: tty=true is
	// `charly shell` (a `-it` TTY, with an ephemeral-run fallback for a
	// stopped pod); tty=false is `charly cmd` (a `-i` inherited-stdin exec
	// into the running container). cmd is the command argv (empty ⇒ an
	// interactive login shell). The host resolves the venue-local command;
	// the owning plugin runs it over the served venue executor via
	// RunInteractive (stdio stays host-side). A non-zero exit is propagated
	// as a typed exit-code error.
	Attach(ctx context.Context, cmd []string, tty bool) error

	// Rebuild is destroy + create + start. Gated on the target's
	// Disposable flag — each implementation must verify it before
	// any destructive action.
	Rebuild(ctx context.Context, opts DeployTargetRebuildOpts) error
}

LifecycleTarget extends UnifiedDeployTarget for live-runtime targets (host, vm, pod). Kubernetes does NOT implement this: its cluster lifecycle is kubectl-managed outside charly. Commands that require a live runtime (charly start/stop/status/logs/shell/rebuild) assert the interface and error uniformly on kubernetes targets.

type LoadedDoc

type LoadedDoc struct {
	// directives is the RAW reserved-directive mapping serialized as YAML bytes (NOT the RawBody
	// JSON the entity bodies use) — so the host replays the ORIGINAL mergeUnifiedDocs decode exactly
	// (`yaml.Unmarshal(directives, &sub)`), honoring the custom YAML unmarshalers on import/discover.
	Directives []byte `yaml:"directives,omitempty" json:"directives,omitempty"`

	Project ParsedProject `yaml:"project,omitempty" json:"project,omitempty"`

	SrcDir string `yaml:"srcDir,omitempty" json:"srcDir,omitempty"`

	SrcLabel string `yaml:"srcLabel,omitempty" json:"srcLabel,omitempty"`
}

#LoadedDoc — one parsed document of a namespace's flattened file tree (root file OR a flat import), in merge order. `directives` is the RAW reserved-directive mapping bytes (version/repo/defaults/provides/discover) the host yaml-decodes into a sub *UnifiedFile before merging (import is consumed by the walk); `project` is the document's decomposed entity nodes; `srcDir` anchors the document's discover paths + labels diagnostics.

type LoadedProject

type LoadedProject struct {
	ID int64 `yaml:"id,omitempty" json:"id,omitempty"`

	Docs []LoadedDoc `yaml:"docs,omitempty" json:"docs,omitempty"`

	Discovered []DiscoveredManifest `yaml:"discovered,omitempty" json:"discovered,omitempty"`

	Namespaces []*NamespaceMount `yaml:"namespaces,omitempty" json:"namespaces,omitempty"`
}

#LoadedProject — the whole WALK result of one project (root OR a mounted namespace): a stable per-walk id (so a namespaced cycle/diamond back-reference resolves to the SAME materialized *UnifiedFile), the ordered documents, the discovered manifests, and the mounted namespace subtrees. The host replays materialize+merge over this to reconstruct the typed *UnifiedFile (identical to the former in-line loadUnifiedInto), then applies the binary-embedded default vocab.

type LoaderExecutor added in v0.2026213.1726

type LoaderExecutor interface {
	// LoaderThreaded returns the CURRENT registry-derived snapshot (recognized kinds / deploy
	// substrates / DeployTraits / ExternalDeploySubstrates / …). Called FRESH at each DATA-seam
	// invocation — NEVER cached at seam-build time — because the walk's connect-declared-kind pass
	// mutates the registry BETWEEN seam construction and the post-walk validators.
	LoaderThreaded() Threaded
	// RunBootstrapPhase invokes every registered bootstrap-phase plugin on the raw root bytes,
	// returning the (possibly transformed) bytes. A leg failure (e.g. a broken reverse-channel
	// HostBuild round trip) is a hard error — never a silent no-op fallback to the raw bytes,
	// which would let LoadUnified proceed on an un-bootstrapped root with zero visible signal.
	RunBootstrapPhase(data []byte) ([]byte, error)
	// WalkProject runs the kind-blind import/discover/namespace walk (the registered ProjectWalker,
	// reached via the host's WalkSeams) → the generic LoadedProject envelope. The host #NodeDoc CUE
	// gate (WalkSeams.GateDoc) runs INSIDE this walk.
	WalkProject(dir string, rootData []byte) (LoadedProject, error)
	// MaterializeLoadedProject replays the host's per-document/per-namespace MATERIALIZE + root-wins
	// MERGE over the walk envelope (registry kind-decode via the registered Materializer).
	MaterializeLoadedProject(lp *LoadedProject, merged *UnifiedFile, byID map[int64]*UnifiedFile) error
	// ValidateAndroidDevices enforces the kind:android box⊻adb XOR — resolves android templates via
	// the provider registry (host-coupled), so a leg, not a pure loaderkit move.
	ValidateAndroidDevices(uf *UnifiedFile) error
	// ValidatePreemptible validates preemptible / requires_exclusive / requires_shared across the
	// deploy map, including the resource-vocabulary cross-check (resolves the resource plugin kind +
	// vm/resource entities via the registry) — host-coupled, so a leg.
	ValidatePreemptible(uf *UnifiedFile) error
}

LoaderExecutor is the typed host-leg contract for the registry-/host-coupled loader steps the kind-blind LoadUnified orchestration cannot do itself: the bootstrap-phase plugin invocation, the registry-coupled import/discover walk, the materialize kind-decode + merge, and the two registry-resolving validators. Promoted here from sdk/loaderkit (#55 loader-keystone) so charly core can hold the host LoaderExecutor implementation while importing ONLY the dedicated spec module — the interface's method signatures already reference only spec types (Threaded / LoadedProject / UnifiedFile), so promoting it is a relocation, not an invention. Because the methods are TYPED, a compiled-in placement pays no envelope tax; only a true out-of-module plugin marshals (the existing LoadedProject / UnifiedFile envelopes). loaderkit.LoadSeamsFromExecutor consumes this to build its internal LoadSeams.

type LoaderWalkRequest

type LoaderWalkRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir"`

	RootData []byte `yaml:"root_data,omitempty" json:"root_data,omitempty"`
}

#LoaderWalkRequest is the "loader-walk" host-builder envelope (K1-LOADER RELOCATION, Unit B/D): a plugin driving loaderkit.LoadUnified plugin-side asks the HOST to run the kind-blind import/discover/namespace walk for a project dir over the ALREADY bootstrap-transformed root bytes. RootData is the raw (bootstrap-phase-transformed) YAML, carried as base64-over-JSON []byte (NOT RawBody — it is YAML, not JSON). The reply is a spec.LoadedProject marshalled directly (no reply envelope needed). Every OTHER loader leg carries an existing type verbatim over the []byte wire: loader-bootstrap ([]byte→[]byte), loader-threaded (∅→spec.Threaded), loader-materialize (spec.LoadedProject→loaderkit.UnifiedFile). The former loader-android-validate / loader-preempt-validate legs dissolved — a plugin-side loader now self-serves those validators over InvokeProvider(kind, OpResolve).

type Local

type Local struct {
	// Ordered candy stack applied to the host. Required (empty list permitted —
	// a staged name-reservation stub; the loader warns, not errors).
	Candy []CandyRef `yaml:"candy,omitempty" json:"candy"`

	InstallOpts *InstallOpts `yaml:"install_opts,omitempty" json:"install_opts,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

type LocalPkg

type LocalPkg struct {
	InstallTemplate string `yaml:"install_template,omitempty" json:"install_template"`

	Probe string `yaml:"probe,omitempty" json:"probe"`

	DownloadTemplate string `yaml:"download_template,omitempty" json:"download_template,omitempty"`
}

#LocalPkg — the local_pkg INSTALL machinery only (the source-build fields pkg_glob/source_sentinel/build_template/dep_builder were removed with the pkg/ source-build cutover: the `charly generate-packages` plugin builds the package now, so the deploy-time + image-build paths only INSTALL the published package via install_template/download_template).

type LocalPkgBuildContext added in v0.2026225.1800

type LocalPkgBuildContext struct {
	// Binary is the in-development charly binary to package (default:
	// os.Executable()).
	Binary string
	// PluginsDir is the dir of in-development plugin binaries + .providers to
	// package (default: the baked plugins dir /usr/lib/charly/plugins/).
	PluginsDir string
	// CalVer is the in-development release CalVer (default: `<Binary> version`).
	CalVer string
	// Arch is the target GOARCH name (default: runtime.GOARCH).
	Arch string
	// CandyYAML is the charly candy's charly.yml — the `--candy` input the
	// plugin reads the packaging section from (default: `<CandyDir>/charly.yml`).
	CandyYAML string
}

LocalPkgBuildContext is the dev-local-pkg build context: the in-development binary + plugins + CalVer + arch + charly.yml the executor uses to build the in-development package via the `charly generate-packages` plugin. Every field is optional — the executor discovers a default when one is empty (see EmitOpts.LocalPkgBuild).

type LocalPkgInstallStep added in v0.2026210.2308

type LocalPkgInstallStep struct {
	// PackageName is the published package name (e.g. "charly") the deploy-time
	// executor downloads from the distro repo.
	PackageName string
	// Version is the release CalVer (e.g. "2026.225.1200") to download.
	Version string
	// CandyName is the candy that declared the packaging section.
	CandyName string
	// CandyDir is the candy's source dir — the anchor for the candy's charly.yml
	// (the `--candy` input the dev-local-pkg build passes to `charly
	// generate-packages`). Set at compile time from the candy's SourceDir.
	CandyDir string

	// Format is the package-format name whose `local_pkg:` config drives this
	// step (e.g. "pac"). "" when the target distro declares no localpkg-capable
	// format — the executor then treats the step as a clean no-op.
	Format string

	// LocalPkg is the format's localpkg contract resolved from the embedded build vocabulary (charly/charly.yml) at
	// compile time (DistroDef.LocalPkgFormat). It carries the install/download
	// templates and probe command — so the executor renders every package-manager
	// command from config instead of hardcoding install/glob literals. The install
	// command auto-resolves the package's dependencies from the target's repos
	// (pacman -U / dnf install / apt-get install), so there is no dep-closure
	// builder. Nil when Format == "".
	LocalPkg *LocalPkg
}

--------------------------------------------------------------------------- LocalPkgInstallStep — install the published charly package from the distro repo onto a deploy target (the download leg). ---------------------------------------------------------------------------

LocalPkgInstallStep is the IR form of a candy's `localpkg:` field — a reference to the published native package (name + version) the deploy-time executor DOWNLOADS from the distro's package repo (the `download_template`) and installs via the `install_template`. It is the proper-package counterpart of the charly candy's ad-hoc curl-a-binary `cmd:` task: on an Arch/CachyOS DEPLOY target the published `.pkg.tar.zst` is downloaded from the distro repo and `pacman -U`-installed — so `charly` lands as the tracked `charly` package at /usr/bin/charly rather than an untracked binary at /usr/local/bin/charly.

(The old source-build form — PkgbuildRef/CandyDir/ProjectDir anchors for a bundled PKGBUILD built on the host via makepkg — was removed with the pkg/ source-build cutover: the `charly generate-packages` plugin builds the package now, so the deploy-time path only INSTALLS the published package.)

Like ApkInstallStep, the step is compiled REGARDLESS of target and each EmitTarget decides whether to execute or skip:

  • the external local deploy (Arch/CachyOS host) and external vm deploy (Arch/CachyOS guest) EXECUTE it via the RunHostStep host-engine leg (download → transfer → pacman -U on the target).
  • On a NON-pac deploy target the executor records a clean skip (a Fedora / Debian host has no pacman; the candy's own `cmd:` task curls the binary there as the documented fallback).
  • The IMAGE build (generate.go writeCandySteps) AND the pod-overlay build-emit RENDER the localpkg IMAGE install via RenderLocalPkgImageInstall (localpkg.go, a PURE sdk/deploykit function since W3): a PRODUCTION box DOWNLOADS the published release, a DISPOSABLE check bed BUILDS the in-development package on the host and COPYs it in (both install via the SAME dep-resolving install template). The pod-overlay build-emit routes through candy/plugin-installstep's `step:local-pkg-install` OpEmit, which STILL calls back the host `step-emit` seam (stepEmitLocalPkgInstall) — not because the render needs core, but to thread the shared `buildEngineContext` alongside its genuinely host-coupled siblings (system-packages/builder/op) on the same seam. A distro with no localpkg-capable format (LocalPkg==nil) renders nothing; the candy's own COPY/curl `cmd:` task is the fallback there.
  • the android / kubernetes substrates (external) SKIP it (no Arch package surface).

func (*LocalPkgInstallStep) Kind added in v0.2026210.2308

func (s *LocalPkgInstallStep) Kind() StepKind

func (*LocalPkgInstallStep) RequiresGate added in v0.2026210.2308

func (s *LocalPkgInstallStep) RequiresGate() Gate

func (*LocalPkgInstallStep) Reverse added in v0.2026210.2308

func (s *LocalPkgInstallStep) Reverse() []ReverseOp

Reverse returns no ledger ops — the package is the deploy substrate's own pacman-tracked package, removed (if ever) via `pacman -R charly` by the operator, not by deploy teardown. Mirrors ApkInstallStep's empty Reverse.

func (*LocalPkgInstallStep) Scope added in v0.2026210.2308

func (s *LocalPkgInstallStep) Scope() Scope

Scope is system — installing a pacman package mutates global package state.

func (*LocalPkgInstallStep) Venue added in v0.2026210.2308

func (s *LocalPkgInstallStep) Venue() Venue

Venue is host-native — the download + install runs host-side (the executor downloads the published package and ships it to the target), then the target installs it via pacman.

type LocalResolveInput

type LocalResolveInput struct {
	Local RawBody `yaml:"local,omitempty" json:"local"`
}

#LocalResolveInput / #AndroidResolveInput carry one opaque template body to project.

type LocalResolveReply

type LocalResolveReply struct {
	Resolved *ResolvedLocal `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#LocalResolveReply / #AndroidResolveReply wrap the resolved template.

type LocalSpec

type LocalSpec = Local

--- top-level entity types ---

type MCPProvideEntry

type MCPProvideEntry struct {
	Name string `yaml:"name,omitempty" json:"name"`

	URL string `yaml:"url,omitempty" json:"url"`

	Transport string `yaml:"transport,omitempty" json:"transport,omitempty"`

	Source string `yaml:"source,omitempty" json:"source"`
}

#MCPProvideEntry is a RESOLVED mcp_provide entry — a CandyMCPProvide (the raw baked label form) plus the Source deployment it came from. Shared by charly's DEPLOY-time provides injection AND the out-of-process mcp CHECK verb (candy/plugin-mcp), which both apply PodAwareMCPProvides; it lives in package spec (ONE copy, R3) so it crosses the charly/plugin module boundary. Its GetName/GetSource methods satisfy charly's Named interface structurally (hand-written behavior, not wire shape).

func PodAwareMCPProvides

func PodAwareMCPProvides(entries []MCPProvideEntry, consumerKey, ctrName string) []MCPProvideEntry

PodAwareMCPProvides rewrites same-deployment entries' URLs to localhost (a peer MCP server in the SAME pod is reached over loopback, not its container-DNS name) and prefers a local entry over a cross-deployment one of the same name. consumerKey is the consuming deployment's box name; ctrName is its running container name. Pure — moved verbatim from charly/provides.go.

func (MCPProvideEntry) GetName

func (e MCPProvideEntry) GetName() string

MCPProvideEntry (schema/candy.cue, generated into cue_types_gen.go) is the CUE-sourced RESOLVED mcp_provide entry. Its GetName/GetSource let it satisfy charly's Named interface (provides.go) — hand-written behavior, not wire shape.

func (MCPProvideEntry) GetSource

func (e MCPProvideEntry) GetSource() string

type MCPServerYAML

type MCPServerYAML = CandyMCPProvide

--- candy sub-types ---

type Marketplace added in v0.2026218.1950

type Marketplace struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Version string `yaml:"version,omitempty" json:"version"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Families map[string]MarketplaceFamily `yaml:"families,omitempty" json:"families"`

	Settings MarketplaceSettings `yaml:"settings,omitempty" json:"settings,omitempty"`
}

CUE schema for the `marketplace` KIND — the single harness/marketplace config entity per repo (the migration of plugins/.claude-plugin/marketplace.json + profiles.json + the harness wiring into candy config). A `marketplace:` node is a top-level entity in a candy-dir file (e.g. candy/charly-marketplace/charly.yml), discovered by the recursive `candy/` walk.

families: one entry per plugin family (the plugins/ directory name). The generator emits plugins/<family>/.claude-plugin/plugin.json + .codex-plugin/plugin.json, plugins/<family>/.mcp.json (when mcp_servers is non-empty), the aggregated plugins/.claude-plugin/marketplace.json and plugins/profiles.json, and the .claude/settings.json plugin-owned keys.

type MarketplaceFamily added in v0.2026218.1950

type MarketplaceFamily struct {
	Category string `yaml:"category,omitempty" json:"category,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Keywords []string `yaml:"keywords,omitempty" json:"keywords,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"`

	McpServers []MarketplaceMCPServer `yaml:"mcp_servers,omitempty" json:"mcp_servers,omitempty"`
}

type MarketplaceMCPServer added in v0.2026218.1950

type MarketplaceMCPServer struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Url string `yaml:"url,omitempty" json:"url,omitempty"`

	Command string `yaml:"command,omitempty" json:"command,omitempty"`

	Args []string `yaml:"args,omitempty" json:"args,omitempty"`
}

type MarketplaceSettings added in v0.2026218.1950

type MarketplaceSettings struct {
	// enabled_plugins — the .claude/settings.json enabledPlugins charly-* entries (each
	// "charly-<family>@<name>"); empty ⇒ enable every declared family.
	EnabledPlugins []string `yaml:"enabled_plugins,omitempty" json:"enabled_plugins,omitempty"`

	// source_path — extraKnownMarketplaces.<name>.source.path (default ./plugins).
	SourcePath string `yaml:"source_path,omitempty" json:"source_path,omitempty"`

	// hooks — the hook entity names to wire into settings.json hooks.<trigger>.
	Hooks []string `yaml:"hooks,omitempty" json:"hooks,omitempty"`
}

type MatchOpMap

type MatchOpMap map[string]any

Matcher operators (validMatcherOps, validate_check.go). A matcher is a scalar (implicit equals), or a single-operator map; #MatcherList accepts a single matcher OR a list. (The base no longer carries a contains-default list def — that shape left with the `file` verb's `contains` field and is now reproduced standalone in the file plugin's #FileContains, decoded with the substring default via decodeContainsList; no base #Op field uses it anymore.)

type Matcher

type Matcher struct {
	Op    string `json:"op"`
	Value any    `json:"value,omitempty"`
}

Matcher is a goss-style matcher: a scalar (implicit equality) or an operator map ({equals: X}, {contains: […]}, {matches: R}, {lt: N}, …).

func (Matcher) MarshalJSON

func (m Matcher) MarshalJSON() ([]byte, error)

MarshalJSON makes the JSON write path the exact inverse of UnmarshalJSON. A ZERO (absent) matcher — Op=="" — marshals to `null`, which UnmarshalJSON maps back to the zero value; without this, the default struct marshal emitted `{"op":""}`, which UnmarshalJSON's single-key-map branch misread as Op="op" (an asymmetry that corrupted every zero matcher round-tripped through JSON — e.g. the all-zero check-matcher fields of an install Op carried in the per-step IR, or a baked-label Plan step). A NON-zero matcher marshals to the SAME canonical `{"op":...,"value":...}` form the default marshaler produced, so real matchers (and existing baked labels) are byte-unchanged.

func (Matcher) MarshalYAML

func (m Matcher) MarshalYAML() (any, error)

MarshalYAML emits the canonical operator-map form so round-tripping is stable.

func (*Matcher) UnmarshalJSON

func (m *Matcher) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the canonical `{"op":"...","value":...}` form, an operator-map (`{"equals": X}`), a bare scalar (`"PONG"` → equals), and `null` / empty (→ the zero matcher, the inverse of MarshalJSON's zero case).

type MatcherList

type MatcherList []Matcher

MatcherList lets users write scalar/single-map shorthand where a list of Matchers is expected. `stdout: PONG` and `stdout: [PONG]` decode identically.

func (*MatcherList) UnmarshalJSON

func (ml *MatcherList) UnmarshalJSON(data []byte) error

UnmarshalJSON mirrors the scalar-shorthand behavior for the JSON read path.

type MaterializeProjectSeams added in v0.2026213.1726

type MaterializeProjectSeams struct {
	// MaterializeProject folds ONE document's parsed entity nodes into uf via the registered
	// Materializer (registry kind-decode + the per-node not-found policy). uf already carries the
	// document's decoded reserved directives; this adds the Box/Candy/Fleet/PluginKinds entities,
	// accumulating across the document's node list.
	MaterializeProject func(pp *ParsedProject, uf *UnifiedFile) error
	// FoldDiscoveredManifests folds every discovered manifest's parsed nodes into uf — a LAYER candy
	// registers a lazy `From:` reference (the bootstrap-critical candyIsImage box⊻layer routing stays
	// host-side), every other kind materializes via the registered Materializer. Shared host-side with
	// charly's ApplyDiscover (R3), so it stays one host leaf.
	FoldDiscoveredManifests func(dms []DiscoveredManifest, uf *UnifiedFile) error
	// ApplyEmbeddedDefaults merges the binary-embedded build vocabulary + sidecar templates UNDER uf's
	// own entries (project-wins). Host-resident: the embedded bytes and the host node-form parser are
	// charly's.
	ApplyEmbeddedDefaults func(uf *UnifiedFile) error
}

MaterializeProjectSeams gathers the host-coupled leaf legs the kind-blind whole-project MATERIALIZE orchestration (loaderkit.MaterializeLoadedProject) calls out to. Each is registry-/host-/bootstrap- coupled and CANNOT run kind-blind inside loaderkit; the host wrapper (charly's hostMaterializeProjectSeams) is the sole constructor and always populates every field. A nil field panics on use. Relocated to the dedicated spec module (#55 2b C3) so the host supplies it as a spec-typed seam via LoaderExecutor.MaterializeProjectSeams() — loaderkit drives the orchestration internally, and charly core stops importing loaderkit to load its own config. All fields are callbacks over spec types (the same injected-seam shape as WalkSeams / MaterializeSeams).

type MaterializeSeams

type MaterializeSeams struct {
	// DecodeEntity resolves pn's discriminator against the provider registry and, if a provider is
	// found, folds the decoded entity into acc (mutating whichever field the kind belongs in) — the
	// SAME dispatch the former in-core normalizeNodeInto/runPluginKind always performed. found=false
	// (no error) means the registry has no provider for pn.Disc — the Materializer applies its OWN
	// not-found policy from there, using Threaded + the callbacks below.
	DecodeEntity func(pn ParsedNode, acc *MaterializedProject) (found bool, err error)
	// BuildFleetEntity folds pn as a deploy-substrate entity into acc.Fleet — the fallback for a
	// RECOGNIZED (Threaded.DeploySubstrates) but not-yet-connected external deploy substrate word.
	BuildFleetEntity func(pn ParsedNode, acc *MaterializedProject) error
	// InKindConnectPass reports whether the loader is inside the re-entrant connect-declared-kind
	// pre-pass (a nested load triggered by connecting a plugin) — a still-unconnected declared kind
	// is silently deferred (skip, no error) during this pass.
	InKindConnectPass func() bool
	// DeclaredKindConnectError returns the retained build/connect failure for a declared-but-
	// unconnected kind word, or nil if it simply hasn't been reached yet.
	DeclaredKindConnectError func(word string) error
}

MaterializeSeams is the set of host-supplied callbacks the per-node kind-decode DISPATCH needs for everything registry-coupled — resolving a node's discriminator word to its live Provider and invoking it stays host-side (boundary law clause M: provider_registry.go + provider_kind_invoke.go are the TRUE mechanism, same bucket as WalkSeams.Boundary/ResolveRef/GateDoc above); the Materializer NEVER touches the provider registry directly.

type MaterializedProject

type MaterializedProject struct {
	Box         map[string]json.RawMessage
	Candy       map[string]json.RawMessage
	Fleet       map[string]FleetNode
	PluginKinds map[string]map[string]json.RawMessage
}

MaterializedProject accumulates the kind-decoded ENTITY maps ONE document's or ONE discovered node's fold produces — the SAME fields charly-core's *UnifiedFile carries for this purpose (Box/Candy/Fleet/PluginKinds); the host copies them in before a Materializer call and back out after (cheap map-header copies — maps are reference types, so this is NOT a deep copy).

The 5 standalone-substrate-TEMPLATE kinds (vm/pod/kubernetes/local/android) do NOT get their own dedicated fields here — they fold into PluginKinds[disc][name] like every other templated kind (distro/builder/init/sidecar/resource/agent already do), so foldStandaloneTemplateReply (charly/node_normalize.go) needs NO per-kind-word switch to pick a destination field: the generic write `acc.PluginKinds[disc][name] = replyJSON` IS the fold, for any disc. charly-core's UnifiedFile.VM()/.Pod()/.Kubernetes()/.Local()/.Android() are now DERIVED accessor methods reading PluginKinds, mirroring the established Distros()/Builders()/Inits() pattern — not stored fields — so there is nothing left to copy for them either.

SDD classification (hand-written, non-wire — precedent: ParsedProject/LoadedProject/CandyRefs/ ScannedCandy above, the established sibling family this type extends): same-process PIPELINE STATE crossing ONLY the compiled-in typed Materializer seam below — never marshaled, because the loader plugin is bootstrap-critical and ALWAYS compiled-in (see the package doc above). A live `cue exp gengotypes` spike on this exact shape (all fields already-portable map[string]json.RawMessage / map[string]FleetNode / map[string]map[string]json.RawMessage — zero disjunctions, zero open tails) would generate a faithful plain struct per the CAN/CANNOT quick reference in /charly-internals:go — CUE-sourcing it is NOT precluded by shape. It stays hand-written here instead because it is not a wire type at all: it never crosses a real marshal/unmarshal boundary (the compiled-in placement passes it as a live *MaterializedProject Go pointer, exactly like WalkSeams' live callbacks above) — the SAME reasoning CandyRefs/ ScannedCandy already document for this file, which the SDD "wire types are CUE-sourced without exception" mandate does not reach (it binds host↔plugin / render-context WIRE carriers; this is same-process pipeline state, the class loader_seam.go's own precedent already carves out).

type Materializer

type Materializer interface {
	MaterializeNode(pn ParsedNode, t Threaded, seams MaterializeSeams, acc *MaterializedProject) error
}

Materializer is the swappable per-node kind-decode DISPATCH seam (#46 unit 1, K1): the loader plugin candy implements it (candy/plugin-loader, delegating to loaderkit.Materialize), and the host resolves the registered loader provider to it and calls it once per parsed entity node — so an alternative loader plugin applies a different not-found/fallback policy by implementing this. Typed (no wire envelope), mirroring DocParser/ProjectWalker/CandyScanner above.

type MergeConfig

type MergeConfig = BoxMerge

--- box / deploy sub-types ---

type MergeReply

type MergeReply struct {
	LayersBefore int `yaml:"layers_before,omitempty" json:"layers_before,omitempty"`

	LayersAfter int `yaml:"layers_after,omitempty" json:"layers_after,omitempty"`

	Skipped bool `yaml:"skipped,omitempty" json:"skipped,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`

	Notes []string `yaml:"notes,omitempty" json:"notes,omitempty"`
}

#MergeReply — the OCI plugin's merge outcome. layers_before/layers_after report the layer-count reduction; skipped is true when the image was too large or had nothing to merge; error carries a per-merge failure (the reply-error convention — e.g. the known podman-load EEXIST case — distinct from an infra Go error on Invoke); notes carries the human progress lines the host prints.

type MergeRequest

type MergeRequest struct {
	ImageRef string `yaml:"image_ref,omitempty" json:"image_ref"`

	MaxMB int `yaml:"max_mb,omitempty" json:"max_mb"`

	MaxTotalMB int `yaml:"max_total_mb,omitempty" json:"max_total_mb"`

	Engine string `yaml:"engine,omitempty" json:"engine"`

	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`
}

#MergeRequest — the host resolves the box (image ref + merge limits + engine) and hands the OCI plugin everything it needs to run the go-containerregistry layer merge, so the plugin never imports charly core config-loading.

type NamespaceMount

type NamespaceMount struct {
	Alias string `yaml:"alias,omitempty" json:"alias,omitempty"`

	Ref bool `yaml:"ref,omitempty" json:"ref,omitempty"`

	RefID int64 `yaml:"refID,omitempty" json:"refID,omitempty"`

	Project LoadedProject `yaml:"project,omitempty" json:"project,omitempty"`
}

#NamespaceMount — one namespaced `import:` (alias → an isolated child project). Two shapes:

  • a DEFINITION (ref=false): the mount carries the child project inline (project), whose own `id` the host registers so a later reference to it shares the SAME materialized *UnifiedFile;
  • a REFERENCE (ref=true): a repo-identity cycle-break or a diamond re-import — the child was already walked, so the mount carries NO inline project, only refID (the target project's id).

The host materialize resolves a reference to the pointer it registered for that id (register- before-recurse), preserving the original loader's pointer identity across the mutual-import cycle (main↔cachyos). Modeled as a LIST entry (not a self-recursive map) so `cue exp gengotypes` generates faithfully — the pointer slice `[]*NamespaceMount` on #LoadedProject breaks recursion.

type OCIEmitStepParams added in v0.2026211.549

type OCIEmitStepParams struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	StepView InstallStepView `yaml:"step_view,omitempty" json:"step_view"`

	PlanView InstallPlanView `yaml:"plan_view,omitempty" json:"plan_view"`
}

#OCIEmitStepParams is the plain-Go param struct the pod-overlay candy marshals into a HostBuild("step-emit", #StepEmitRequest{Word:"oci-emit-step", Payload:<#OCIEmitStepParams>}) per install step (promoted from sdk/deploykit — #55 import-purity, Cone V value-vocabulary). Dir is the project dir the host "oci-emit-step" emitter uses to look up the cached overlay buildEngineContext (a live *Generator/DistroDef/BuilderConfig/Box cannot cross the wire); StepView is the step's wire form (StepToView) and PlanView the step's plan wire form — the host reconstructs both (StepFromView/PlanFromView) then calls ociEmitStep, returning the rendered Containerfile fragment. StepView/PlanView are required (always set by the caller); Dir is optional (empty for the non-overlay per-step emit path).

type Op

type Op struct {
	// --- verb discriminators (exactly one set; Go Kind() enforces) ---
	Mkdir string `yaml:"mkdir,omitempty" json:"mkdir,omitempty"`

	Copy string `yaml:"copy,omitempty" json:"copy,omitempty"`

	Write string `yaml:"write,omitempty" json:"write,omitempty"`

	Link string `yaml:"link,omitempty" json:"link,omitempty"`

	Download string `yaml:"download,omitempty" json:"download,omitempty"`

	Setcap string `yaml:"setcap,omitempty" json:"setcap,omitempty"`

	Build string `yaml:"build,omitempty" json:"build,omitempty"`

	// plugin — the generic PLUGIN-VERB discriminator, INTERNAL-ONLY: the
	// parse-time desugar rewrites every authored `<word>: <input>` sugar key
	// into this plugin/plugin_input pair, and authoring plugin:/plugin_input:
	// directly in a step is a hard load error (run: charly migrate). The pair
	// stays on #Op because it is the wire/label form (the desugared tree this
	// def validates, the ai.opencharly.description JSON) and the Go dispatch
	// surface (providerRegistry.ResolveVerb). plugin_input is validated by the
	// PLUGIN's own served CUE schema (not base #Op).
	Plugin string `yaml:"plugin,omitempty" json:"plugin,omitempty"`

	// --- shared modifiers ---
	ID string `yaml:"id,omitempty" json:"id,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Skip bool `yaml:"skip,omitempty" json:"skip,omitempty"`

	Timeout Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`

	// command — INTERNAL-ONLY rehydration target (never authored): the `command`
	// plugin verb's INSTALL-EMIT copies plugin_input.command here for emitCmd
	// (build) / renderOpCommand (deploy). Authored `command:` on a step IS the
	// command plugin's sugar key (scalar = the primary shorthand), consumed by
	// the parse-time desugar before this def validates; the wl/libvirt argv
	// moved into those plugins' own input defs.
	Command string `yaml:"command,omitempty" json:"command,omitempty"`

	Context []Context `yaml:"context,omitempty" json:"context,omitempty"`

	// `pod:` (per-step container venue) is RETIRED — a step's execution venue is
	// derived ENTIRELY from its position in the fleet tree (flattenFleetVenues
	// → Op.venue, yaml:"-"). Authoring it is a closed-schema rejection (run:
	// charly migrate).
	DependsOn []string `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`

	// plugin_input — generic params for a `plugin:` verb. Opaque to base #Op
	// (accepts any shape); the plugin's own spliced CUE schema validates it.
	PluginInput map[string]any `yaml:"plugin_input,omitempty" json:"plugin_input,omitempty"`

	// --- install/build modifiers ---
	RunAs string `yaml:"run_as,omitempty" json:"run_as,omitempty"`

	To string `yaml:"to,omitempty" json:"to,omitempty"`

	Target string `yaml:"target,omitempty" json:"target,omitempty"`

	Caps string `yaml:"caps,omitempty" json:"caps,omitempty"`

	Content string `yaml:"content,omitempty" json:"content,omitempty"`

	Extract string `yaml:"extract,omitempty" json:"extract,omitempty"`

	ExtractInclude []string `yaml:"extract_include,omitempty" json:"extract_include,omitempty"`

	// unless_exists — capability GATE for an install step: when the named path already
	// exists in the image, the step is a no-op. It exists because a plan step has no
	// build-time distro filter (`exclude_distro:` is read by the CHECK runner, never by
	// the emitter), so a candy whose artifact is supplied by the distro package on SOME
	// distros and by an upstream archive on the rest had no declarative way to express
	// "fetch only where the package did not already provide this". Without it the only
	// recourse was to abandon the verb and hand-roll `curl`, which silently forfeits what
	// the verb provides: the content-addressed /tmp/downloads cache, the atomic .part
	// rename, and `cache:` mounts. Detection rather than a distro name, so any distro that
	// later gains the package is picked up with no edit.
	UnlessExists string `yaml:"unless_exists,omitempty" json:"unless_exists,omitempty"`

	StripComponents int `yaml:"strip_components,omitempty" json:"strip_components,omitempty"`

	Uninstall []string `yaml:"uninstall,omitempty" json:"uninstall,omitempty"`

	Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`

	Cache []string `yaml:"cache,omitempty" json:"cache,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	// --- step modifiers ---
	Eventually Duration `yaml:"eventually,omitempty" json:"eventually,omitempty"`

	RetryInterval Duration `yaml:"retry_interval,omitempty" json:"retry_interval,omitempty"`

	// `on:` (cross-member driver dispatch) is RETIRED — a step that drives a
	// peer/driver is authored as a step CHILD of that member node; its venue is
	// derived from position (flattenFleetVenues → Op.venue). Authoring `on:` is
	// a closed-schema rejection (run: charly migrate).
	Tag []string `yaml:"tag,omitempty" json:"tag,omitempty"`

	// Origin is populated at collection time (candy:<name>/box:<name>/deploy-*),
	// NEVER authored (Go yaml:"-"), but it TRAVELS in the ai.opencharly.description
	// OCI-label JSON (Go json:"origin") — so the generated spec.Op MUST carry it
	// for a faithful drop-in (do NOT @go(-) it). gengotypes emits
	// `yaml:"origin,omitempty" json:"origin,omitempty"`, matching the hand tag.
	Origin string `yaml:"origin,omitempty" json:"origin,omitempty"`

	// venue + intent_do are RUNTIME-DERIVED, never authored: venue is stamped
	// from a step's fleet-tree position (flattenFleetVenues), intent_do from
	// the enclosing Step's keyword (run→act / check→assert / agent→instruct).
	// They are generated onto the Go struct (the check runner persists them on
	// the Op-by-value passed to runOne / EffectiveDo), but the #Step authoring
	// arms forbid them (`venue?: _|_, intent_do?: _|_`) so authoring either is a
	// closed-schema rejection — exactly the contract the retired `pod:`/`on:`
	// fields enforced. yaml:"-" (never read from YAML); json omits them when
	// empty (the bake-time state, so they never leak into the description label).
	Venue string `yaml:"venue,omitempty" json:"venue,omitempty"`

	IntentDo string `yaml:"intent_do,omitempty" json:"intent_do,omitempty"`

	// --- file/copy/write SHARED modifier ---
	// `mode` is the SHARED octal-permission modifier: the copy/write install verbs read
	// Op.Mode at deploy (the external local/vm deploy walk via kit.ParseTaskMode),
	// so it STAYS in #Op. The file-EXCLUSIVE fields (file/exists/owner/group_of/filetype/
	// contains/sha256) LEFT #Op — they are read ONLY by the `file` plugin verb and now live
	// in its #FileInput (candy/plugin-file, with the contains-default semantic
	// reproduced via the candy's decodeContainsList). The state-provision migrator MOVES `mode` into a
	// file step's plugin_input while LEAVING it here for copy/write (the shared-companion
	// pattern, like gid between unix_group and user).
	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	// exclude_distro — a SHARED step-level skip filter read by the generic runOne for
	// EVERY verb (skip the step when any image distro tag intersects the list), NOT a
	// package-exclusive field, so it STAYS on #Op. The `package`-exclusive fields
	// (installed/version/package_map) MOVED into #PackageInput when `package` extracted.
	ExcludeDistros []string `yaml:"exclude_distro,omitempty" json:"exclude_distro,omitempty"`

	// --- shared assertion matchers (SHARED via matchAll: the command/http probe
	// plugins + every live-verb plugin assert exit_status/stdout/stderr off the
	// marshalled step Op, so they STAY in #Op; verb-exclusive request/input
	// fields live in each plugin's input def) ---
	ExitStatus *int `yaml:"exit_status,omitempty" json:"exit_status,omitempty"`

	Stdout MatcherList `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	Stderr MatcherList `yaml:"stderr,omitempty" json:"stderr,omitempty"`
}

#Op is the unified operation vocabulary (Go: charly/checkspec.go Op) AFTER the parse-time desugar: the builtin install verbs + the genuinely SHARED step modifiers + the internal plugin/plugin_input pair every plugin-verb sugar key desugars into. CLOSED: an unknown key on the desugared step is a typo (a plugin word never reaches this def — the desugar consumed it). Per-verb fields live in each plugin's own input def, served over Describe.

func (*Op) Kind

func (c *Op) Kind() (string, error)

Kind returns the single verb set on the Op, or an error when zero or multiple are set (the exactly-one-verb rule).

func (*Op) PluginInputStr

func (c *Op) PluginInputStr(verb string) string

PluginInputStr returns the string value of plugin_input[verb] when this Op is a `plugin: <verb>` op carrying that key — "" otherwise. The ONE home (R3) for the host check runner (checkrun.go) AND the k8sgen plugin's probe builder (candy/plugin-k8sgen), both of which read a migrated `plugin: http`/`addr`/`file`/`command` check subject.

func (*Op) StringFields

func (c *Op) StringFields() []*string

StringFields returns pointers to every string-valued modifier field, for the in-place ${VAR} expansion driven by ExpandVars (free function in main).

func (*Op) VerbsSet

func (c *Op) VerbsSet() []string

type OpStep added in v0.2026210.2308

type OpStep struct {
	Op           *Op
	CandyName    string
	CandyDir     string
	CtxPath      string // absolute layer-dir path replacing "/ctx/" on host
	ResolvedUser string // uid:gid or "root" after resolveUserSpec

	// To is the home-resolved copy/download destination for the DEPLOY path.
	// The compiler tokenizes the task's `to:` (~ / ${HOME} / $HOME → {{.Home}})
	// here; InstallPlan.ResolveHome substitutes the destination's real home at
	// emit. execTask uses this (not the raw Task.To) for PutFile, so a
	// `copy: to: ${HOME}/...` lands in the actual guest/host home instead of a
	// literal "${HOME}" directory under sudo (HOME=/root). Empty when the task
	// has no `to:`. The OCI build emitter reads Task.To directly (build-time
	// ENV expands ${HOME}), so it is unaffected by this field.
	To string

	// CandyVars are the candy manifest `vars:` map propagated into the task
	// script as exports. Build-time gets these via Containerfile ENV
	// directives (emitVarsEnv); host/local-deploy time has no equivalent
	// mechanism, so the renderer emits `export K=V` lines from this field.
	// Without this, candies like `kubernetes` whose download URLs reference
	// ${K3D_VERSION} fetched an empty path-component at deploy time and
	// curl 404'd.
	CandyVars map[string]string

	// Distros is the image's distro tag chain (img.Tags), carried so the act-emit
	// enabler can pass it to a state-provision plugin verb's RenderProvisionScript
	// (e.g. the cross-distro package resolution). Empty for non-plugin ops; the
	// unix_group act ignores it, but a future package/service state-provision plugin
	// reads it.
	Distros []string
}

OpStep wraps a raw Op so the IR compiler doesn't have to transform it further. The OCI target and host target both understand Op shapes (cmd / mkdir / copy / write / link / download / setcap / build) and emit the appropriate directives. CtxPath substitutes for /ctx/ in cmd bodies on the host target (container RUN directives keep /ctx via bind-mount, which doesn't exist on the host).

func (*OpStep) Kind added in v0.2026210.2308

func (s *OpStep) Kind() StepKind

func (*OpStep) RequiresGate added in v0.2026210.2308

func (s *OpStep) RequiresGate() Gate

func (*OpStep) Reverse added in v0.2026210.2308

func (s *OpStep) Reverse() []ReverseOp

func (*OpStep) Scope added in v0.2026210.2308

func (s *OpStep) Scope() Scope

func (*OpStep) Venue added in v0.2026210.2308

func (s *OpStep) Venue() Venue

type OverlayBuildReply

type OverlayBuildReply struct {
	OverlayRef string `yaml:"overlay_ref,omitempty" json:"overlay_ref,omitempty"`

	BaseImage string `yaml:"base_image,omitempty" json:"base_image,omitempty"`

	DeployName string `yaml:"deploy_name,omitempty" json:"deploy_name,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`

	// resolved_project is the overlay-scoped resolved-project envelope the
	// candy constructs a deploykit.Generator from. Nil when there is no
	// add_candy overlay to synthesize (the tag-only path).
	ResolvedProject *ResolvedProject `yaml:"resolved_project,omitempty" json:"resolved_project,omitempty"`

	// plans is the deployment's compiled InstallPlans serialized as
	// InstallPlanViews. Empty for the no-overlay path.
	Plans []InstallPlanView `yaml:"plans,omitempty" json:"plans,omitempty"`

	// calver is the host's current CalVer.
	CalVer string `yaml:"calver,omitempty" json:"calver,omitempty"`

	// overlay_candy_security carries each overlay candy's own `security:`
	// block.
	OverlayCandySecurity map[string]*Security `yaml:"overlay_candy_security,omitempty" json:"overlay_candy_security,omitempty"`

	// parent_volumes carries the PARENT deploy node's bind-mount volumes for a
	// NESTED pod-in-pod overlay build.
	ParentVolumes []DeployVolume `yaml:"parent_volumes,omitempty" json:"parent_volumes,omitempty"`
}

#OverlayBuildReply is what the "overlay" host-builder returns.

type OverlayBuildRequest

type OverlayBuildRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	DeployName string `yaml:"deploy_name,omitempty" json:"deploy_name,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`

	AssumeYes bool `yaml:"assume_yes,omitempty" json:"assume_yes,omitempty"`

	AllowRepoChanges bool `yaml:"allow_repo_changes,omitempty" json:"allow_repo_changes,omitempty"`

	AllowRootTasks bool `yaml:"allow_root_tasks,omitempty" json:"allow_root_tasks,omitempty"`

	WithServices bool `yaml:"with_services,omitempty" json:"with_services,omitempty"`
}

#OverlayBuildRequest is the BUILD-ENGINE DISPATCH envelope for the pod-overlay build — the F10 "overlay" host-builder.

type PackageItem

type PackageItem struct {
	Name        string `yaml:"name" json:"name"`
	Description string `yaml:"description,omitempty" json:"description,omitempty"`
}

--------------------------------------------------------------------------- PackageItem — #PackageItem. Bare scalar XOR object form; the bare-scalar shorthand is canonicalized to {name} by the loader normalizer. ---------------------------------------------------------------------------

type PackageSection

type PackageSection struct {
	FormatName string `yaml:"format_name,omitempty" json:"format_name,omitempty"`

	Packages []string `yaml:"packages,omitempty" json:"packages,omitempty"`

	Raw map[string]any `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#PackageSection — a generic format-specific package section (rpm/deb/pac/aur). Raw carries the full YAML map for template rendering. Mirrors deploykit.PackageSection (which now aliases this).

type Packaging added in v0.2026225.1508

type Packaging struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Description string `yaml:"description,omitempty" json:"description"`

	Maintainer string `yaml:"maintainer,omitempty" json:"maintainer"`

	Homepage string `yaml:"homepage,omitempty" json:"homepage,omitempty"`

	License string `yaml:"license,omitempty" json:"license,omitempty"`

	Section string `yaml:"section,omitempty" json:"section,omitempty"`

	Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`

	// variants — the named plugin-set variants. The format's `default_variant`
	// (or the variant named "default" when unset) packages as the plain `charly`
	// package; every other named variant packages as `charly-<variant>`.
	Variants map[string]*PackagingVariant `yaml:"variants,omitempty" json:"variants,omitempty"`

	// formats — per-format (nFPM name) dependency + default-variant metadata.
	Formats map[string]*PackagingFormat `yaml:"formats,omitempty" json:"formats,omitempty"`
}

#Packaging — the native-package metadata section (candy `packaging:`): the common fields (name/description/maintainer/…) + a `variants:` map (named plugin-set variants → `charly-<variant>` packages) + a per-format map (`formats.<fmt>`: deps + the format's default variant). CLOSED. The per-format keys use the nFPM format names (deb/rpm/apk/archlinux/ipk/msix).

type PackagingFormat added in v0.2026225.1508

type PackagingFormat struct {
	Depends []string `yaml:"depends,omitempty" json:"depends,omitempty"`

	// optdepends — the Arch `optdepend = <pkg>: <desc>` map (nFPM does not emit
	// optdepends; sdk/packagekit post-processes the .pkg.tar.zst to inject them).
	OptDepends map[string]string `yaml:"optdepends,omitempty" json:"optdepends,omitempty"`

	Recommends []string `yaml:"recommends,omitempty" json:"recommends,omitempty"`

	Suggests []string `yaml:"suggests,omitempty" json:"suggests,omitempty"`

	// default_variant — the variant name packaged as the plain `charly` package
	// for this format (defaults to "default" when unset).
	DefaultVariant string `yaml:"default_variant,omitempty" json:"default_variant,omitempty"`

	// publisher — the msix Publisher display name (msix-specific).
	Publisher string `yaml:"publisher,omitempty" json:"publisher,omitempty"`

	// properties — the msix Properties (msix-specific).
	Properties map[string]string `yaml:"properties,omitempty" json:"properties,omitempty"`
}

#PackagingFormat — per-format (nFPM name) packaging metadata.

type PackagingVariant added in v0.2026225.1508

type PackagingVariant struct {
	Description string `yaml:"description,omitempty" json:"description"`

	// plugins — the default-plugin words this variant ships at
	// /usr/lib/charly/plugins/ (a subset of the released plugin tarball).
	Plugins []string `yaml:"plugins,omitempty" json:"plugins"`
}

#PackagingVariant — one named plugin-set variant.

type Pacstrap

type Pacstrap struct {
	BasePackages []string `yaml:"base_package,omitempty" json:"base_package,omitempty"`

	ExtraRepos []PacstrapRepo `yaml:"extra_repo,omitempty" json:"extra_repo,omitempty"`

	RuntimePacmanConf string `yaml:"runtime_pacman_conf,omitempty" json:"runtime_pacman_conf,omitempty"`
}

type PacstrapRepo

type PacstrapRepo struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Server string `yaml:"server,omitempty" json:"server"`

	SigLevel string `yaml:"siglevel,omitempty" json:"siglevel,omitempty"`
}

type ParsedNode

type ParsedNode struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Disc string `yaml:"disc,omitempty" json:"disc"`

	Body RawBody `yaml:"body,omitempty" json:"body,omitempty"`

	Children []*ParsedNode `yaml:"children,omitempty" json:"children,omitempty"`
}

#ParsedNode — one decomposed reserved-word node: its name, kind discriminator, the opaque entity body (the complete kind value as JSON, materialized per-kind by the host), and any sub-entity member children (deployable kinds only). The recursive Children mirror the deploy tree's member nesting the host folds into uf.Fleet.

type ParsedPortMapping added in v0.2026213.1726

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

ParsedPortMapping describes the four possible shapes podman accepts:

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

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

func ParsePortMapping added in v0.2026213.1726

func ParsePortMapping(mapping string) (ParsedPortMapping, bool)

ParsePortMapping is the canonical port-mapping parser.

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

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

type ParsedProject

type ParsedProject struct {
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Nodes []ParsedNode `yaml:"nodes,omitempty" json:"nodes,omitempty"`

	Imports []string `yaml:"imports,omitempty" json:"imports,omitempty"`
}

#ParsedProject — the whole parse result: the schema version (for the post-parse gate), the decomposed reserved-word nodes, and the resolved import refs (already fetched + merged by the plugin's import walk). Discover results are decomposed into the same nodes list.

type ParsedRef

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

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

func ParseRemoteRef

func ParseRemoteRef(ref string) *ParsedRef

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

type Phase

type Phase int

Phase identifies which template phase (prepare / install / cleanup) a rendered step belongs to. The phase lets targets treat repo-config mutation (prepare) distinctly from package install — which is exactly the granularity --allow-repo-changes gates. A single format section in the candy manifest typically emits three PhasePrepare/PhaseInstall/PhaseCleanup steps, and the compiler tags each with the appropriate phase.

const (
	PhasePrepare Phase = iota
	PhaseInstall
	PhaseCleanup
)

func (Phase) String

func (p Phase) String() string

type PhaseCarrier

type PhaseCarrier interface {
	PluginPhase() string
}

PhaseCarrier is implemented by a provider (grpcProvider out-of-proc, inprocProvider compiled-in) that carries its declared lifecycle PHASE (F9). A provider not implementing it (e.g. a builtin non-plugin provider) is treated as PhaseRuntime by the host's phaseOfProvider.

type PhaseSet

type PhaseSet struct {
	Prepare *PhaseTemplates `yaml:"prepare,omitempty" json:"prepare,omitempty"`

	Install *PhaseTemplates `yaml:"install,omitempty" json:"install,omitempty"`

	Cleanup *PhaseTemplates `yaml:"cleanup,omitempty" json:"cleanup,omitempty"`
}

Three-phase template set: prepare → install → cleanup, each with a container (Containerfile) and host (shell) rendering. Template bodies are Go text/template — strings, never parsed here.

type PhaseTemplates

type PhaseTemplates struct {
	// install is the ONE venue-agnostic package-install body (repo setup + key
	// import + package install, written with `&& \` continuations valid in both
	// plain shell and a Dockerfile RUN). FormatPhaseTemplate wraps it per venue:
	// the container venue prepends `RUN {{cacheMounts .CacheMounts}} \`, the host
	// venue runs it as-is. host/container remain as venue-specific OVERRIDES for
	// the prepare/cleanup phases and for any install that genuinely differs by
	// venue (R3: one canonical body, venue applied at render, never two copies).
	Install string `yaml:"install,omitempty" json:"install,omitempty"`

	Container string `yaml:"container,omitempty" json:"container,omitempty"`

	Host string `yaml:"host,omitempty" json:"host,omitempty"`
}

type Plugin

type Plugin struct {
	// providers: the "<class>:<word>" reserved-word capabilities this plugin
	// serves (e.g. "verb:exampleprobe", "kind:my-thing"). Each is registered into
	// providerRegistry — built-in (init()) or out-of-tree (gRPC).
	Providers []PluginCapability `yaml:"providers,omitempty" json:"providers"`

	// source: "builtin" (Go compiled into the charly binary, init()-registered) OR
	// a git ref (github.com/org/repo[/sub][@tag]) fetched via the @github resolver +
	// built into a provider binary. Default builtin.
	Source string `yaml:"source,omitempty" json:"source"`

	// primary: verb word → the input field its scalar sugar shorthand targets
	// (`file: /x` → plugin_input: {<primary>: "/x"}). Declared in the MANIFEST so
	// the byte-gated prescan registers it BEFORE the out-of-process provider
	// connects (the parse-time desugar needs it pre-parse); the served
	// ProvidedCapability.Primary mirrors it for the compiled-in placement.
	Primary map[string]string `yaml:"primary,omitempty" json:"primary,omitempty"`
}

#Plugin — the candy's plugin declaration. Its presence makes the candy a plugin (Go: charly/checkspec.go via the generated Candy.Plugin field, consumed by plugin_loader.go). CLOSED.

type PluginCapability

type PluginCapability string

#PluginCapability — a "<class>:<word>" capability string. class ∈ the closed ProviderClass set; word is lowercase-hyphenated.

type Pod

type Pod struct {
	// References a kind:box (bare lowercase-hyphenated name or remote ref).
	// Optional: the Go field has no non-empty validator.
	Box CandyRef `yaml:"box,omitempty" json:"box,omitempty"`

	// Each entry is a SidecarConfig wrapper: a single-key map of named
	// sidecar templates (PodSpec.Sidecar []SidecarConfig).
	Sidecar []PodSidecar `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`

	Secret []DeploySecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	EnvDefaults map[string]string `yaml:"env_default,omitempty" json:"env_default,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

type PodAttachOpts

type PodAttachOpts struct {
	Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"`

	Tty bool `yaml:"tty,omitempty" json:"tty"`

	Shell PodShellOpts `yaml:"shell,omitempty" json:"shell,omitempty"`

	CmdOpts PodCmdOpts `yaml:"cmd_opts,omitempty" json:"cmd_opts,omitempty"`
}

#PodAttachOpts carries the F12 Attach op's parameters (K4/F12 inversion): tty selects the shell resolver (interactive `charly shell`) vs the cmd resolver (`charly cmd`'s non-interactive `-i` exec) — mirrors the former host-side resolvePodAttachPlan dispatch, now run plugin-side.

type PodCmdOpts

type PodCmdOpts struct {
	Sidecar string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

#PodCmdOpts carries `charly cmd`'s per-invocation extra (--sidecar), replacing the former host-side resolvePodCmdPlan.

type PodCmdPayload added in v0.2026217.301

type PodCmdPayload struct {
	Command string `yaml:"command,omitempty" json:"command,omitempty"`

	Sidecar string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

#PodCmdPayload — see #PodLifecycleRequest's header; op="cmd". Carries `charly cmd <box> <command>`'s per-invocation fields: the host does ONLY the irreducible dispatchLifecycleTarget("cmd") + LifecycleTarget.Attach step, mirroring op="shell" exactly — the interactive `-i` exec runs over the SAME host-held exec.RunInteractive leg (stdio never crosses the wire). The plugin owns the CLI grammar + the completion notification itself.

type PodConfigBoxEngineReply

type PodConfigBoxEngineReply struct {
	Engine string `yaml:"engine,omitempty" json:"engine"`
}

type PodConfigBoxEngineRequest

type PodConfigBoxEngineRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	GlobalEngine string `yaml:"global_engine,omitempty" json:"global_engine"`
}

#PodConfigBoxEngineRequest / Reply: ResolveBoxEngineForDeploy(box,instance,globalEngine) — reads the per-host deploy config's Engine override. A thin wrapper distinct from the (now-deleted) load-deploy seam since callers here want only the resolved engine string, not the whole FleetConfig.

type PodConfigCleanDeployEntryReply

type PodConfigCleanDeployEntryReply struct {
}

type PodConfigCleanDeployEntryRequest

type PodConfigCleanDeployEntryRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#PodConfigCleanDeployEntryRequest / Reply: deploykit.CleanDeployEntry(box, instance, marshalDeployNode) — the `charly remove` deploy-entry cleanup (Cutover B unit 2 remove-verb completion). Follows the {box!, instance?} → {} host-owns-load+lock+mutate+save shape — deliberately NOT the plugin-side deploykit.SaveDeployState/SaveFleetConfig write (fleet import/reset + deploy-state persist, #55 K4 — no host seam), which persists an ALREADY-LOADED, already-mutated whole FleetConfig with no internal load/lock/entry-removal logic — a genuinely different, narrower operation CleanDeployEntry's own internal file-lock + entry-removal + provides-cleanup + empty-file-delete logic cannot be reduced to.

type PodConfigContainerTunnelReply

type PodConfigContainerTunnelReply struct {
	TunnelJSON RawBody `yaml:"tunnel_json,omitempty" json:"tunnel_json,omitempty"`
}

type PodConfigContainerTunnelRequest

type PodConfigContainerTunnelRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#PodConfigContainerTunnelRequest / Reply: reads the RUNNING container's baked image ref (containerImage), extracts + merges its metadata, and resolves the tunnel config. Distinct from #PodConfigTunnelResolveRequest (which takes an already-resolved MetaJSON) — this seam resolves the image/metadata itself from a container name. candy/plugin-deploy-pod's start/stop path builds its own tunnel plan locally now (enc_tunnel_resolve.go, wave γ); this seam STAYS registered because candy/plugin-pod's `charly remove` teardown path (remove_tunnel.go) is a separate, still-live caller.

type PodConfigMigrateSecretsReply

type PodConfigMigrateSecretsReply struct {
	ConfigJSON RawBody `yaml:"config_json,omitempty" json:"config_json"`

	Migrated int `yaml:"migrated,omitempty" json:"migrated,omitempty"`
}

type PodConfigMigrateSecretsRequest

type PodConfigMigrateSecretsRequest struct {
	ConfigJSON RawBody `yaml:"config_json,omitempty" json:"config_json"`

	MetaJSON RawBody `yaml:"meta_json,omitempty" json:"meta_json"`

	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#PodConfigMigrateSecretsRequest / Reply: MigratePlaintextEnvSecret(dc, meta, box, instance) — the one-time plaintext-env → credential-store migration (file backup + DefaultCredentialStore + saveFleetConfigNodeForm, all FINAL/K5-deferred registry-coupled inventory per the ledger). config_json carries the ALREADY-LOADED dc (the plugin's own loaded overlay — the former #PodConfigLoadDeployRequest is deleted, K-wave 2 cone R3) so the host mutates + re-saves the SAME loaded structure the plugin is mid-flow with, never a stale reload.

type PodConfigRemoveReply

type PodConfigRemoveReply struct {
}

#PodConfigRemoveReply is the OpConfigRemove handler's reply — empty.

type PodConfigRemoveRequest

type PodConfigRemoveRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#PodConfigRemoveRequest carries `charly config remove`'s flags (the former BoxConfigRemoveCmd's authored fields — distinct from `charly remove`/#PodLifecycleRequest op="remove"+#PodRemovePayload, which tears down the whole deploy; this removes only the quadlet + disables the service). Dispatched to the deploy:pod plugin's OpConfigRemove handler VERBATIM as Params, peer-to-peer from candy/plugin-pod's ConfigRemoveCmd (the former HostBuild("pod-config-remove") forwarder is DELETED, K-wave 2 cone R3).

type PodConfigSaveFleetReply added in v0.2026219.1057

type PodConfigSaveFleetReply struct {
}

type PodConfigSaveFleetRequest added in v0.2026219.1057

type PodConfigSaveFleetRequest struct {
	ConfigJSON RawBody `yaml:"config_json,omitempty" json:"config_json"`
}

#PodConfigSaveFleetRequest / Reply: saveFleetConfigNodeForm(dc) — persists a (plugin-mutated) *deploykit.FleetConfig back through the SAME loader-coupled seam.

type PodConfigScrubCliEnvReply

type PodConfigScrubCliEnvReply struct {
	Cleaned []string `yaml:"cleaned,omitempty" json:"cleaned,omitempty"`

	Imported int `yaml:"imported,omitempty" json:"imported,omitempty"`
}

type PodConfigScrubCliEnvRequest

type PodConfigScrubCliEnvRequest struct {
	CliEnv []string `yaml:"cli_env,omitempty" json:"cli_env,omitempty"`

	MetaJSON RawBody `yaml:"meta_json,omitempty" json:"meta_json"`
}

#PodConfigScrubCliEnvRequest / Reply: scrubSecretCLIEnv(cliEnv, meta) — the credential-store Set() pre-scrub for `-e NAME=VAL` flags declared secret_accepts/secret_requires.

type PodConfigSetupReply

type PodConfigSetupReply struct {
	SidecarList SidecarList `yaml:"sidecar_list,omitempty" json:"sidecar_list,omitempty"`
}

#PodConfigSetupReply is the OpConfigSetup handler's reply — empty for the normal setup path, mirroring #PodLifecycleReply's empty-for-every-op-but-cmd shape. The `--list-sidecars` leaf (r.list_sidecars) fills sidecar_list instead: the plugin's own embedded template library (sidecar_embedded.go — the "pod-config-list-sidecars" seam is DELETED, K-wave 2 cone R3), so candy/plugin-pod prints it in the charly CLI's own stdio (the out-of-process plugin's stdout is go-plugin-discarded).

type PodConfigSetupRequest

type PodConfigSetupRequest struct {
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Build bool `yaml:"build,omitempty" json:"build,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	Clean bool `yaml:"clean,omitempty" json:"clean,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	Port []string `yaml:"port,omitempty" json:"port,omitempty"`

	KeepMounted bool `yaml:"keep_mounted,omitempty" json:"keep_mounted,omitempty"`

	Password string `yaml:"password,omitempty" json:"password,omitempty"`

	RefreshSecret []string `yaml:"refresh_secret,omitempty" json:"refresh_secret,omitempty"`

	VolumeFlag []string `yaml:"volume_flag,omitempty" json:"volume_flag,omitempty"`

	Bind []string `yaml:"bind,omitempty" json:"bind,omitempty"`

	Encrypt []string `yaml:"encrypt,omitempty" json:"encrypt,omitempty"`

	MemoryMax string `yaml:"memory_max,omitempty" json:"memory_max,omitempty"`

	MemoryHigh string `yaml:"memory_high,omitempty" json:"memory_high,omitempty"`

	MemorySwapMax string `yaml:"memory_swap_max,omitempty" json:"memory_swap_max,omitempty"`

	Cpus string `yaml:"cpus,omitempty" json:"cpus,omitempty"`

	Seed bool `yaml:"seed,omitempty" json:"seed,omitempty"`

	ForceSeed bool `yaml:"force_seed,omitempty" json:"force_seed,omitempty"`

	DataFrom string `yaml:"data_from,omitempty" json:"data_from,omitempty"`

	UpdateAll bool `yaml:"update_all,omitempty" json:"update_all,omitempty"`

	SSHKey string `yaml:"ssh_key,omitempty" json:"ssh_key,omitempty"`

	Sidecar []string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`

	ListSidecars bool `yaml:"list_sidecars,omitempty" json:"list_sidecars,omitempty"`

	NoAutoDetect bool `yaml:"no_autodetect,omitempty" json:"no_autodetect,omitempty"`

	// explicit_ref is set programmatically (never authored) by `charly fleet from-box`'s
	// source-less deploy path (from_box_pod.go) — the P13-KERNEL direction-flip carries
	// it across the wire now that the ORCHESTRATION (formerly reading the kong:"-" Go field
	// directly) moved into the plugin.
	ExplicitRef string `yaml:"explicit_ref,omitempty" json:"explicit_ref,omitempty"`

	// host_env_json is the marshalled #HostEnv (CharlyBin/Home/Version) — the SAME R10
	// bed-found bug class #DeployTargetDispatchRequest's own host_env_json field documents
	// (unified_targets.go's hostEnvJSON(): os.Executable() resolves correctly to the charly
	// binary only when called IN CORE — a plugin's os.Executable() resolves to the PLUGIN
	// binary, wrong for an out-of-process placement). Setup's quadlet emission needs the REAL
	// charly binary path for the encrypted-mount ExecStartPre line
	// (deploykit.QuadletConfig.CharlyBin); the bug was dormant here until a deploy actually had
	// an encrypted volume to mount (check-enc-pod's R10 first exercised it once the
	// project-declared-volume fallback started resolving one). The HOST computes it (core's
	// hostEnvJSON(), R3 — one host-identity helper) and threads it as DATA on the OpRun dispatch
	// envelope; candy/plugin-pod forwards it verbatim into this field (the former
	// hostBuildPodConfigSetup forwarder is DELETED, K-wave 2 cone R3).
	HostEnvJSON RawBody `yaml:"host_env_json,omitempty" json:"host_env_json,omitempty"`
}

#PodConfigSetupRequest carries the `charly config [setup]` command flags (the former BoxConfigSetupCmd's authored fields, PLUS explicit_ref — from_box_pod.go's programmatically-set source-less-deploy field, below). P13-KERNEL direction-flip: the deploy:pod plugin's sdk.OpConfigSetup handler receives it VERBATIM as Params. The former HostBuild("pod-config-setup") forwarder is DELETED (K-wave 2 cone R3) — candy/plugin-pod's ConfigSetupCmd (and plugin-fleet's from_box_pod.go) dispatch the op peer-to-peer via InvokeProvider; the plugin RUNS the former runConfig orchestration (candy/plugin-deploy-pod/config_setup.go).

type PodConfigTunnelResolveReply

type PodConfigTunnelResolveReply struct {
	TunnelJSON RawBody `yaml:"tunnel_json,omitempty" json:"tunnel_json,omitempty"`
}

type PodConfigTunnelResolveRequest

type PodConfigTunnelResolveRequest struct {
	MetaJSON RawBody `yaml:"meta_json,omitempty" json:"meta_json"`
}

#PodConfigTunnelResolveRequest / Reply: TunnelConfigFromMetadata(meta) — resolves the tunnel config (charly.yml overlay applied) from image labels.

type PodConfigWriteReply

type PodConfigWriteReply struct {
	WrittenPaths []string `yaml:"written_paths,omitempty" json:"written_paths,omitempty"`
}

#PodConfigWriteReply returns the paths the plugin actually wrote (deterministic; the host already knows them — used for the byte-parity assertion + teardown provenance).

type PodConfigWriteRequest

type PodConfigWriteRequest struct {
	PodConfigJSON RawBody `yaml:"pod_config_json,omitempty" json:"pod_config_json"`

	ContainerPath string `yaml:"container_path,omitempty" json:"container_path"`

	PodPath string `yaml:"pod_path,omitempty" json:"pod_path,omitempty"`

	SidecarPaths map[string]string `yaml:"sidecar_paths,omitempty" json:"sidecar_paths,omitempty"`

	TunnelPath string `yaml:"tunnel_path,omitempty" json:"tunnel_path,omitempty"`

	CloudflaredCfgPath string `yaml:"cloudflared_cfg_path,omitempty" json:"cloudflared_cfg_path,omitempty"`
}

#PodConfigWriteRequest carries the POD config-WRITE (P11). Under Ruling C the config-WRITE (the quadlet/.pod/sidecar/tunnel file generation) moved to the deploy:pod plugin, while the RESOLVE + host side-effects (secret provisioning, saveDeployState, enc-mount, data-seed, systemctl) stay in the HOST `charly config` command (Q1=(a)). So this is HOST→PLUGIN: for a pod deploy, `charly config` resolves the full QuadletConfig + computes the exact target PATHS (the core filename helpers, unchanged) and PUSHES them to the plugin's config-write Op, which generates the file contents (deploykit.GenerateQuadlet + the pod/sidecar/tunnel generators) and os.WriteFiles them — byte-identical to the former core write phase (same paths, same content, same modes: .container/.pod/sidecar 0600, tunnel .service 0644).

PodConfigJSON is the resolved deploykit.QuadletConfig — a hand-written runtime type with no CUE def, so it travels as an opaque RawBody envelope (the VmJSON pattern; no new CUE wire struct). An optional path field being SET is the host's signal to write that file kind (pod_path/sidecar_paths present ⇒ sidecars configured; tunnel_path present ⇒ cloudflare tunnel) — the host owns the write conditionals, the plugin writes what it is told.

type PodDisposableReply

type PodDisposableReply struct {
	Disposable bool `yaml:"disposable,omitempty" json:"disposable,omitempty"`
}

#PodDisposableReply carries the single overlay-disposability bit.

type PodDisposableRequest

type PodDisposableRequest struct {
	Name string `yaml:"name,omitempty" json:"name"`
}

#PodDisposableRequest asks the host whether a per-host POD deploy overlay entry is disposable (K5-U2/3). This is the ONE AI-harness check-project fact the resolved-project envelope cannot carry: the harness's iterate sandbox is an OPERATOR-provisioned per-host deploy (`charly fleet add <sandbox> <ref> --disposable`), so its disposability lives in the per-host overlay (LoadFleetConfig → ~/.config/charly/charly.yml), NOT the project charly.yml the resolved-project envelope projects (Mode Purity keeps the overlay out of the build-mode projection). The overlay read needs the core loader a plugin cannot import, and no deploy/status provider serves it, so it rides this THIN retained host seam. The host returns Fleet[Name].IsDisposable() (false when the sandbox has no entry — the harness then skips its fresh-per-run restart). Class-generic action noun "pod-disposable" (F11 — never a substrate word).

type PodExecReply

type PodExecReply struct {
	Output string `yaml:"output,omitempty" json:"output,omitempty"`

	ExitCode int `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`
}

#PodLifecyclePlan is the pod-lifecycle carrier (the K4 deep-body move). Formerly host-resolved and threaded on OpStart/OpStop; candy/plugin-deploy-pod now SELF-RESOLVES it (resolve.go's resolveStartQuadlet/resolveStopPlan, resolve_direct.go's resolveStartDirect) from the deploy key + the raw CLI opts (#PodStartOpts/#PodStopOpts), reaching the host only for genuinely host-only mechanisms (the deploy-overlay HostBuild seam, verb:credential, verb:enc, verb:tunnel) — this type is now built AND consumed entirely within the plugin process. It EXECUTES it — running the container start/stop over the served host executor and composing enc + tunnel via InvokeProvider(verb:enc/verb:tunnel), so the former podCli("start"/"stop"/…) `charly`-reentries are DELETED (bodies, not shells). The pre-built enc verb input (spec.EncExecInput — a hand-written wire type with no CUE def) rides as an opaque RawBody envelope (empty ⇒ that leg is skipped, the common plain-pod case) with its Method set per-op host-side; tunnel references the CUE-def'd #TunnelConfig directly and the plugin infers start-vs-stop from the op. The ARBITER claim is NOT threaded here — its CHARLY_PREEMPT_LEASE machinery is host-PROCESS state a placement-agnostic plugin cannot own, so the host proxy BRACKETS the plugin op (acquire before OpStart; release after OpStop + on the failure path). #PodExecReply is the reply from the pod plugin's OpShell CAPTURED-exec leg (the K4 `charly service` move — an in-container init-mgmt exec, non-interactive). The plugin RunCaptures the argv over the served executor and returns the combined Output + the exact ExitCode; the host reprints Output (placement-agnostic: an out-of-process plugin's stdout is NOT charly's) and propagates a non-zero ExitCode as *sdk.ExitCodeError so `charly service` preserves the container command's exit code exactly (the passthrough→capture semantics change the ruling requires be exit-code-faithful).

type PodLifecyclePlan

type PodLifecyclePlan struct {
	Mode string `yaml:"mode,omitempty" json:"mode"`

	SvcName string `yaml:"svc_name,omitempty" json:"svc_name,omitempty"`

	ContainerName string `yaml:"container_name,omitempty" json:"container_name"`

	RunArgv []string `yaml:"run_argv,omitempty" json:"run_argv,omitempty"`

	DirectDeploy bool `yaml:"direct_deploy,omitempty" json:"direct_deploy,omitempty"`

	EngineBin string `yaml:"engine_bin,omitempty" json:"engine_bin"`

	Unmount bool `yaml:"unmount,omitempty" json:"unmount,omitempty"`

	Enc RawBody `yaml:"enc,omitempty" json:"enc,omitempty"`

	Tunnel *TunnelConfig `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`
}

type PodLifecycleReply added in v0.2026217.301

type PodLifecycleReply struct {
	ExitCode int `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`
}

#PodLifecycleReply is the "pod-lifecycle" host-builder reply. exit_code is populated only for op="cmd" — the container command's own exit code, so `charly cmd`'s process exit propagates it (the plugin reconstructs an *sdk.ExitCodeError from it) — it cannot ride the HostBuild ERROR return, which stringifies the typed error; it must ride a reply FIELD, exactly as the former __cmd/CliReply.ExitCode path did. Every other op's reply is empty; op-specific progress prints host-side (the compiled-in plugin's HostBuild runs in charly's own process) and failure signals via the error return.

type PodLifecycleRequest added in v0.2026217.301

type PodLifecycleRequest struct {
	Op string `yaml:"op,omitempty" json:"op"`

	Box string `yaml:"box,omitempty" json:"box"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	// node is the per-host deploy overlay entry the command:pod / command:cmd plugin ALREADY
	// resolved plugin-side (loaderkit.ResolveLifecycleDeployNodeViaExecutor, the cycle-free
	// plugin-side overlay read) and threads as DATA — so the host's dispatchLifecycleTarget
	// operates on the passed *spec.Deploy instead of re-reading the per-host config itself (the
	// config-READ is a plugin loading capability, not a host M — #55 K4 seam-completion). Seven of
	// the eight ops carry it (start/stop/shell/logs/service/cmd + update — which resolves the
	// deploy node from the merged tree plugin-side since K-wave 2 cone CONTESTED) and remove
	// needs no node at all (it only releases the arbiter claim) — absent for that one.
	Node *Deploy `yaml:"node,omitempty" json:"node,omitempty"`

	// payload is the op-specific #PodXPayload, JSON-marshalled by the calling command plugin and
	// re-decoded host-side once op is known (mirrors the plugin wire protocol's own
	// Operation.Params json.RawMessage design — there is no parallel envelope-vs-payload type
	// system, R3).
	Payload RawBody `yaml:"payload,omitempty" json:"payload,omitempty"`
}

#PodLifecycleRequest is the ONE discriminated request every pod-lifecycle HostBuild op (start/stop/shell/logs/service/cmd/update/remove) sends over the single "pod-lifecycle" HostBuild kind (#55 W3 A10b). Converges the seam on the codebase's own established op-discriminated wire idiom — #ArbiterInvokeInput's flat action-multiplexed shape, charly/provider.go's own Operation.Params json.RawMessage envelope — which the former 8-per-verb #PodXRequest family (one CUE type + one HostBuild kind string per verb, each redeclaring box/instance/node) was the last outlier against. op selects which #PodXPayload type `payload` unmarshals into (host_build_pod_lifecycle_dispatch.go's hostBuildPodLifecycle switch); box/instance/node — common to nearly every op — hoisted OUT of the per-op payloads into this shared envelope (R3).

type PodLiveStdioPlan

type PodLiveStdioPlan struct {
	// resolved venue command:
	//
	//	shell → `podman exec -it charly-<box> bash [-c cmd]` OR the ephemeral `podman run --rm -it … bash`
	//	cmd   → `<engine> exec [-e env] charly-<box>[-<sidecar>] sh -c <command>` (no -t; stdin piped)
	//	logs  → `<engine> logs [-f] [-n N] charly-<box>` OR quadlet `journalctl --user -u <svc> [-f] [-n N]`
	Script string `yaml:"script,omitempty" json:"script"`
}

#PodLiveStdioPlan is the F12 LIVE-STDIO carrier — ONE carrier for shell + cmd + logs (identical shape, R3; the op + the executor method distinguish them). Formerly host-resolved and threaded on OpAttach/OpLogs op.Params; candy/plugin-deploy-pod now SELF-RESOLVES it (resolve_f12.go's resolveAttachPlan/resolveShellPlan/resolveCmdPlan/resolveLogsPlan) from the deploy key + #PodAttachOpts/#PodLogsOpts, so this type is now built AND consumed entirely within the plugin process — no wire crossing for the plan itself, only for the opts that drive it. The plugin EXECUTES it over the served host executor via exec.RunInteractive (OpAttach — inherited LIVE stdin/stdout/stderr; the child `podman exec -it`/`-i` owns the PTY + resize + Ctrl-C) / exec.RunStream (OpLogs — inherited LIVE stdout/stderr, no stdin). UNARY: the host reverse-server runs IN the charly process, so os.Stdin/os.Stdout = the operator's terminal — stdio NEVER crosses the wire, only the session exit code (the hostBuildCli doctrine). This takes the F12 exit for the shell/cmd/logs-follow rows of the #57 M-core register: the former inline `charly shell`/`cmd` bodies + the podCli("logs") reentry are DELETED (bodies, not shells).

type PodLogsOpts

type PodLogsOpts struct {
	Follow bool `yaml:"follow,omitempty" json:"follow,omitempty"`

	Tail int `yaml:"tail,omitempty" json:"tail,omitempty"`

	Sidecar string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

#PodLogsOpts carries `charly logs [-f]`'s parameters (K4/F12 inversion), replacing the former host-side resolvePodLogsPlan. Mirrors spec.DeployTargetLogsOpts (Follow/Tail/Sidecar) — the plugin-side wire twin for the pod logs payload, distinct from the dispatch-envelope DeployTargetLogsOpts (K-wave 2 cone R5: the charly-core LogsOpts type is DELETED).

type PodLogsPayload added in v0.2026217.301

type PodLogsPayload struct {
	Follow bool `yaml:"follow,omitempty" json:"follow,omitempty"`

	Sidecar string `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`
}

#PodLogsPayload — see #PodLifecycleRequest's header; op="logs". The former LogsCmd's authored fields (F12 — the host resolves the journalctl/`<engine> logs` stream command, the owning plugin streams it live to the operator's stdio).

type PodRemovePayload added in v0.2026217.301

type PodRemovePayload struct {
	Purge bool `yaml:"purge,omitempty" json:"purge,omitempty"`

	KeepDeploy bool `yaml:"keep_deploy,omitempty" json:"keep_deploy,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`
}

#PodRemovePayload — see #PodLifecycleRequest's header; op="remove". The former RemoveCmd's authored fields — the host orchestration this ONE op still performs is just the arbiter-release bracket (host_build_pod_lifecycle_dispatch.go's hostBuildPodRemove); the rest of remove's orchestration (quadlet/companion-service teardown, pre_remove hooks, purge, deploy-entry cleanup) runs entirely in candy/plugin-pod now.

type PodResolveInput

type PodResolveInput struct {
	Pod RawBody `yaml:"pod,omitempty" json:"pod"`
}

#PodResolveInput carries one opaque pod template body to project.

type PodResolveReply

type PodResolveReply struct {
	Resolved *ResolvedPod `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#PodResolveReply wraps the resolved pod template.

type PodServicePayload added in v0.2026217.301

type PodServicePayload struct {
	Argv []string `yaml:"argv,omitempty" json:"argv"`
}

#PodServicePayload — see #PodLifecycleRequest's header; op="service". Carries the FULLY plugin-resolved argv for `charly service start/stop/status/restart` (Cutover B unit 2 completion): the plugin now performs resolveServiceInit/validateServiceName/execInitCommand's argv-building itself (all portable — spec.ResolvedInit is already an sdk alias, buildkit.RenderTemplate is sdk-portable) and sends the FINAL `<engine> exec <container> <tool> <op> [svc]` argv; the host does ONLY the irreducible dispatchLifecycleTarget + LifecycleTarget.Shell step.

type PodShellOpts

type PodShellOpts struct {
	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	VolumeFlag []string `yaml:"volume_flag,omitempty" json:"volume_flag,omitempty"`

	Bind []string `yaml:"bind,omitempty" json:"bind,omitempty"`

	NoAutoDetect bool `yaml:"no_auto_detect,omitempty" json:"no_auto_detect,omitempty"`

	Interactive bool `yaml:"interactive,omitempty" json:"interactive"`

	WrapPTY bool `yaml:"wrap_pty,omitempty" json:"wrap_pty"`
}

#PodShellOpts carries `charly shell`'s per-invocation CLI extras (K4/F12 inversion): the plugin self-resolves the #PodLiveStdioPlan using these + the deploy key, replacing the former host-side resolvePodShellPlan/buildShellArgs/buildExecArgs. interactive/wrap_pty are HOST-RESOLVED booleans (interactive = force_tty || isTerminal(); wrap_pty = force_tty && !isTerminal()) — the plugin is a subprocess whose own stdout is not the operator's terminal, so the tty check MUST happen host-side at the moment of the real CLI invocation and cross the wire as data, never be re-derived plugin-side.

type PodShellPayload added in v0.2026217.301

type PodShellPayload struct {
	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Command string `yaml:"command,omitempty" json:"command,omitempty"`

	Build bool `yaml:"build,omitempty" json:"build,omitempty"`

	TTY bool `yaml:"tty,omitempty" json:"tty,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	VolumeFlag []string `yaml:"volume_flag,omitempty" json:"volume_flag,omitempty"`

	Bind []string `yaml:"bind,omitempty" json:"bind,omitempty"`

	NoAutoDetect bool `yaml:"no_autodetect,omitempty" json:"no_autodetect,omitempty"`
}

#PodShellPayload — see #PodLifecycleRequest's header; op="shell". The former ShellCmd's authored fields (F12 — the host resolves the venue command, the owning plugin runs it over the served venue executor via RunInteractive, stdio host-held).

type PodSidecar

type PodSidecar struct {
	Sidecar map[string]Sidecar `yaml:"sidecar,omitempty" json:"sidecar"`
}

#PodSidecar mirrors Go SidecarConfig: a single-key wrapper around a map of named sidecar templates. CLOSED (the struct has exactly one field).

type PodSpec

type PodSpec = Pod

--- top-level entity types ---

type PodStartOpts

type PodStartOpts struct {
	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Port []string `yaml:"port,omitempty" json:"port,omitempty"`

	VolumeFlag []string `yaml:"volume_flag,omitempty" json:"volume_flag,omitempty"`

	Bind []string `yaml:"bind,omitempty" json:"bind,omitempty"`

	NoAutoDetect bool `yaml:"no_auto_detect,omitempty" json:"no_auto_detect,omitempty"`
}

#PodStartOpts carries `charly start`'s direct-mode CLI extras (K4 inversion, quadlet-mode first): the plugin now SELF-RESOLVES the #PodLifecyclePlan (over the deploy-overlay HostBuild seam + the already-portable sdk resolvers) using these opts + the deploy key already on lifecycleParams.Name — replacing the former host-side resolvePodStartPlan. The quadlet path ignores every field (mirrors the pre-inversion contract — CLI extras apply only to direct mode).

type PodStartPayload added in v0.2026217.301

type PodStartPayload struct {
	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Build bool `yaml:"build,omitempty" json:"build,omitempty"`

	Env []string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Port []string `yaml:"port,omitempty" json:"port,omitempty"`

	VolumeFlag []string `yaml:"volume_flag,omitempty" json:"volume_flag,omitempty"`

	Bind []string `yaml:"bind,omitempty" json:"bind,omitempty"`

	NoAutoDetect bool `yaml:"no_autodetect,omitempty" json:"no_autodetect,omitempty"`
}

#PodStartPayload — see #PodLifecycleRequest's header; op="start". The former StartCmd's authored fields (DEPLOY-wave CLI-struct port): the command:pod plugin owns the CLI GRAMMAR but cannot drive the LifecycleTarget dispatch (ResolveTarget, the plugin loader — core Mechanisms), so `charly start`'s command is THIN — it forwards these flags, and the host runs the existing startViaLifecycle orchestration VERBATIM, exactly as `charly fleet add` stayed core behind HostBuild("resolve-target-add").

type PodStopOpts

type PodStopOpts struct {
	Unmount bool `yaml:"unmount,omitempty" json:"unmount,omitempty"`
}

#PodStopOpts carries `charly stop --unmount` (K4 inversion): the plugin self-resolves the STOP #PodLifecyclePlan using this + the deploy key, replacing the former host-side resolvePodStopPlan.

type PodStopPayload added in v0.2026217.301

type PodStopPayload struct {
	Unmount bool `yaml:"unmount,omitempty" json:"unmount,omitempty"`
}

#PodStopPayload — see #PodLifecycleRequest's header; op="stop". The former StopCmd's authored fields.

type PodUpdatePayload added in v0.2026217.301

type PodUpdatePayload struct {
	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Build bool `yaml:"build,omitempty" json:"build,omitempty"`

	Seed bool `yaml:"seed,omitempty" json:"seed,omitempty"`

	ForceSeed bool `yaml:"force_seed,omitempty" json:"force_seed,omitempty"`

	DataFrom string `yaml:"data_from,omitempty" json:"data_from,omitempty"`
}

#PodUpdatePayload — see #PodLifecycleRequest's header; op="update". Since K-wave 2 cone CONTESTED the resolved deploy node rides the #PodLifecycleRequest.node field (the plugin resolves it plugin-side from the merged tree); the former tree_json field is DELETED.

type PortMapping

type PortMapping struct {
	HostIP string `yaml:"host_ip,omitempty" json:"host_ip,omitempty"`

	HostPort int `yaml:"host_port,omitempty" json:"host_port"`

	CtrPort int `yaml:"container_port,omitempty" json:"container_port"`

	Proto string `yaml:"protocol,omitempty" json:"protocol"`
}

#PortMapping — one published port's structured runtime mapping (host IP/port -> container port/proto). Surfaces on #DeploymentStatus so renderers + host probes consume it without re-parsing.

type PortPin

type PortPin = string

--- named-pattern scalars (string) ---

type PortScope

type PortScope struct {
	All     bool
	Ports   []int
	PortMap map[int]string // port → hostname (cloudflare only)
}

PortScope handles three YAML forms for public/private port specification: "all" → All; [443, 8443] → Ports; {18789: "host"} → PortMap.

func (PortScope) IsZero

func (p PortScope) IsZero() bool

func (PortScope) MarshalJSON

func (p PortScope) MarshalJSON() ([]byte, error)

func (PortScope) MarshalYAML

func (p PortScope) MarshalYAML() (any, error)

func (*PortScope) UnmarshalJSON

func (p *PortScope) UnmarshalJSON(data []byte) error

func (*PortScope) UnmarshalYAML

func (p *PortScope) UnmarshalYAML(value *yaml.Node) error

type PortSpec

type PortSpec struct {
	Port     int
	Protocol string
}

PortSpec is the canonical {port, protocol} form of a candy `port:` entry. Source: charly/layers.go PortSpec (no struct tags — yaml.v3 lowercases the field name).

type PostTeardownReply

type PostTeardownReply struct {
	RemoveEntries []string `yaml:"remove_entries,omitempty" json:"remove_entries,omitempty"`
}

#PostTeardownReply is the OpPostTeardown reply: the host removes each named charly.yml deploy-entry key AFTER the plugin's teardown.

type PreemptLease

type PreemptLease struct {
	Claimant string `yaml:"claimant,omitempty" json:"claimant"`

	Claim HolderAddr `yaml:"claim,omitempty" json:"claim"`

	Tokens []string `yaml:"tokens,omitempty" json:"tokens"`

	Shared bool `yaml:"shared,omitempty" json:"shared,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Transient bool `yaml:"transient,omitempty" json:"transient"`

	Preempted []PreemptedHolder `yaml:"preempted,omitempty" json:"preempted"`

	Created string `yaml:"created,omitempty" json:"created"`

	// owner_pid/owner_start identify the OUTERMOST process that created the lease — the liveness
	// signal a concurrent charly process's reconcile uses (leaseLive).
	OwnerPID int64 `yaml:"owner_pid,omitempty" json:"owner_pid,omitempty"`

	OwnerStart string `yaml:"owner_start,omitempty" json:"owner_start,omitempty"`
}

#PreemptLease is one active resource claim — exclusive (a VM with sole use) or shared (a refcounted pod claim; many coexist on one token).

type PreemptLedger

type PreemptLedger struct {
	Leases []PreemptLease `yaml:"leases,omitempty" json:"leases"`
}

#PreemptLedger is the on-disk lease set (~/.local/share/charly/preemption/leases.yml).

type PreemptedHolder

type PreemptedHolder struct {
	Addr HolderAddr `yaml:"addr,omitempty" json:"addr"`

	Holds []string `yaml:"holds,omitempty" json:"holds"`

	Restore string `yaml:"restore,omitempty" json:"restore"`
}

#PreemptedHolder records one holder a lease stopped, its declared exclusive tokens, and its restore policy — so ReleaseClaimant/reconcile restart exactly what was stopped.

type PreemptibleConfig

type PreemptibleConfig struct {
	Holds   []string `yaml:"holds,omitempty" json:"holds,omitempty"`
	Stop    string   `yaml:"stop,omitempty" json:"stop,omitempty"`
	Restore string   `yaml:"restore,omitempty" json:"restore,omitempty"`
}

PreemptibleConfig is the HOLDER side of the resource-arbitration axis.

type PrepareVenueReply

type PrepareVenueReply struct {
	Venue VenueDescriptor `yaml:"venue,omitempty" json:"venue"`

	State RawBody `yaml:"state,omitempty" json:"state,omitempty"`

	Notes []string `yaml:"notes,omitempty" json:"notes,omitempty"`
}

#PrepareVenueReply is the OpPrepareVenue reply. Venue is re-materialized host-side into a live DeployExecutor (the live executor never crosses the wire); State is an opaque deploy-entry patch the host persists; Notes are human-facing lines the host prints.

type Process

type Process interface {
	Stdin() io.WriteCloser
	Stdout() io.ReadCloser
	Stderr() io.ReadCloser
	Wait() error
	Close() error
}

Process is one exact-argv, full-duplex child running in a deployment venue. It is deliberately a behaviour contract rather than a wire shape: target routes and all data sent over the process are CUE-generated, while this seam only exposes the OS pipes that carry the generated gRPC protocol.

Close must be idempotent, close stdin, terminate the complete child process group when it has not exited, and reap it. Wait may be called more than once.

type ProcessExecutor

type ProcessExecutor interface {
	StartProcess(ctx context.Context, launch ProcessLaunch) (Process, error)
}

ProcessExecutor is an optional capability implemented by deployment executors that can host a bidirectional process. Keeping it separate from DeployExecutor preserves compatibility with capture-only executors and test doubles. Callers must return ErrNotSupported when this capability is absent.

type ProcessLaunch

type ProcessLaunch struct {
	Argv []string `yaml:"argv,omitempty" json:"argv"`

	WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`
}

type ProjectLoader added in v0.2026213.1726

type ProjectLoader interface {
	LoadUnified(dir string, exec LoaderExecutor) (*UnifiedFile, bool, error)
	// DecodeEntityViaCUE is the kind-blind per-entity CUE decode mechanism (shorthand normalize +
	// CUE-ingest + Decode), relocated to loaderkit (K1 unit 1): normalizes node against t's shape
	// (shorthand expansion, scalar→string coercion), then CUE-decodes the result into out. node must
	// BE the entity value (a candy body / a single kind entity / an assembled node-form body), not a
	// kind-keyed wrapper; does not mutate the input node. Compiled-in only (the loader is
	// bootstrap-critical), no wire envelope — every kind/candy/node-form decode in charly core
	// routes through this seam instead of importing loaderkit directly.
	DecodeEntityViaCUE(node *yaml.Node, t reflect.Type, out any, label string) error
	// ValidateEntityClosedCUE unifies a single entity with #<Kind> and validates
	// it WITHOUT requiring concreteness — closedness violations (unknown keys) and type/enum/regex
	// conflicts, but not missing-required fields (K1 unit 2 relocation).
	ValidateEntityClosedCUE(kind, label string, entity cue.Value) error
	// ValidateEntityCUE is the CONCRETE twin of the above — closedness PLUS missing-required
	// fields and unresolved disjunctions. The schema-tightening corpus (charly's
	// cue_tighten_test.go) drives it as the regression guard that the modeled subtrees stay
	// strict; it needs the host's registry-derived verb primaries to parse its candy cases, so it
	// runs host-side and reaches the loader's compiled schema through this seam.
	ValidateEntityCUE(kind, label string, entity cue.Value) error
	// CueDocFromYAML ingests one YAML document into a cue.Value (the whole doc), built with the
	// loader's own cue.Context so the result can Unify against its compiled schema (K1 unit 2
	// relocation; the host-threaded schema handle was dropped in K-wave 2 cone R1).
	CueDocFromYAML(path string, data []byte) (cue.Value, error)
	// ValidateNodeDocCUE validates a unified node-form document (raw YAML bytes) by unifying EACH
	// top-level entity node against #Node — the load-time "validate-before-execute" structural gate
	// (K1 unit 2 relocation).
	ValidateNodeDocCUE(label string, data []byte) error
	// ApplyCueDefaults fills schema-declared defaults into an already-RESOLVED entity by unifying
	// its marshaled form with #<Kind> and decoding back (K1 unit 2 relocation).
	ApplyCueDefaults(kind string, out any) error
	// ResolveMergedDeployTree returns the top-level Fleet (deploy-node) map — the merged project
	// charly.yml + per-host operator overlay, ready for dotted-path traversal — the host-side
	// merged-tree read the check host seams (deployNodePluginContext + check_venue_resolve) need.
	// It is the merged-tree sibling of LoadUnified: LoadUnified returns the PROJECT-only tree
	// (loadmodel.go Fleet has no overlay field), so a caller that needs the per-host operator
	// overlay merged in routes through THIS seam instead. The merge LOGIC (the loaderkit
	// project+overlay projection+merge) stays in the ONE copy in sdk/loaderkit
	// (loaderkit.ResolveMergedTreeViaExecutor); the host reaches it through this compiled-in seam
	// instead of importing loaderkit directly (#55 coneA Q2(1) — charly core's check_cmd.go sheds
	// its loaderkit import). The in-proc executor is threaded on ctx via sdk.ContextWithExecutor
	// (the SAME in-proc reverse-channel path ExecutorForInvoke uses for Invoke) so the seam
	// signature stays spec-typed — the plugin-side impl retrieves it via sdk.ExecutorFromContext.
	// Compiled-in only (the loader is bootstrap-critical), no wire envelope.
	ResolveMergedDeployTree(ctx context.Context, dir string) (map[string]FleetNode, error)
	// MaterializeLoadedProject replays the whole-project per-document/per-namespace MATERIALIZE +
	// root-wins MERGE over a walk envelope, driving loaderkit's kind-blind orchestration with the
	// host-supplied per-node seams — so charly core reaches materialize WITHOUT importing loaderkit
	// (#55 2b C3). The host's own LoadUnified path AND the loader-materialize HostBuild seam (serving a
	// plugin-side loader) both route through here.
	MaterializeLoadedProject(lp *LoadedProject, merged *UnifiedFile, byID map[int64]*UnifiedFile, seams MaterializeProjectSeams) error
	// MarshalMaterialized marshals a materialized UnifiedFile into the wire envelope the
	// loader-materialize HostBuild seam returns to a plugin-side loader (it captures the nested
	// plugin-kind maps loaderkit-internally, so the host reaches it through this seam).
	MarshalMaterialized(uf *UnifiedFile) ([]byte, error)
	// ValidateAndroidDevices enforces the kind:android box⊻adb XOR; the host supplies the
	// registry-resolve callback (the validation LOGIC stays in loaderkit).
	ValidateAndroidDevices(uf *UnifiedFile, resolveAndroid func(json.RawMessage) (*ResolvedAndroid, error)) error
	// ValidatePreemptible validates preemptible / requires_exclusive / requires_shared across the
	// deploy map; the host supplies the registry-resolve callbacks (the validation LOGIC stays in
	// loaderkit).
	ValidatePreemptible(uf *UnifiedFile, resolveResource func(json.RawMessage) (*ResolvedResource, error), resolveVm func(json.RawMessage) (*ResolvedVm, error)) error
	// ScanCandyFromLocal runs the candy-scan fetch fix-point (remote-ref collect, fetch, per-entity
	// version arbitration, host-completion + finalize) over a local candy set, driving the host-coupled
	// legs through the caller-supplied ScanSeams closures — so charly core reaches the scan MECHANISM
	// through this compiled-in seam instead of importing loaderkit (#55 C3b-ii). The scan LOGIC stays in
	// the ONE copy in sdk/loaderkit (candy/plugin-build reaches it directly, being a plugin).
	ScanCandyFromLocal(localScanned map[string]ScannedCandy, initCfg *InitConfig, seams ScanSeams) (map[string]CandyReader, error)
	// RunDiscover walks the flat generic discover: scan-spec list, parsing each discovered manifest via
	// the host-supplied WalkSeams — the discover half of the loader mechanism, reached via the seam so
	// charly core never imports loaderkit for it.
	RunDiscover(rootDir string, specs []ScanSpec, seams WalkSeams) ([]DiscoveredManifest, error)
	// FinalizeScannedCandies is the scan pipeline's finalize choke point (host-completion + bare-string
	// the refs + wrap into the FINAL CandyReader). It is deploykit-coupled and stays in loaderkit; the
	// host reaches it through this seam — the dominant shared choke point across charly's scan call sites.
	FinalizeScannedCandies(scanned map[string]ScannedCandy, initCfg *InitConfig) map[string]CandyReader

	// IsResourceDisc reports whether a discriminator names a deploy-substrate kind (the markers of a
	// fleet member / fleet-shaped node) — the CUE-derived #ResourceKind vocab, OR a recognized
	// external deploy substrate word (t.DeploySubstrates).
	IsResourceDisc(d string, t Threaded) bool
	// FleetTargetForDisc maps a node discriminator to the FleetNode Target — DATA-driven via
	// t.DeployTraits: a word with no declared deploy traits is TARGETLESS (e.g. group).
	FleetTargetForDisc(d string, t Threaded) string
	// SetFleetCrossRef sets the deploy's cross-ref from a scalar discriminator value — DATA-driven
	// via t.DeployTraits' ImageBacked trait (image-backed → dn.Image; otherwise → dn.From). A
	// targetless word (no declared traits) sets neither.
	SetFleetCrossRef(dn *FleetNode, disc, ref string, t Threaded)
	// IsStandaloneResourceKind reports whether disc names one of the substrate kinds that are BOTH a
	// standalone TEMPLATE and a deploy — DATA-driven via t.DeployTraits (same fact
	// FleetTargetForDisc/SetFleetCrossRef resolve against).
	IsStandaloneResourceKind(disc string, t Threaded) bool
	// FoldStandaloneTemplateReply folds a standalone-template kind's echoed reply JSON into acc's
	// generic PluginKinds[disc][name] map — the C2-substrate TEMPLATE fold arm, GENERIC by
	// construction (no per-kind-word switch).
	FoldStandaloneTemplateReply(disc, name string, replyJSON json.RawMessage, acc *MaterializedProject) error

	// AssembleEntityBody returns the DOCUMENT-wrapped entity-body mapping to decode: pn's body
	// value (an empty mapping when the value is null/absent or a scalar cross-ref).
	AssembleEntityBody(pn ParsedNode) (*yaml.Node, error)
	// DecodeNodeValue decodes pn's body via the shared CUE entity decoder into out (a *struct) —
	// the SAME entity-body assembler + CUE decode every candy/kind/node-form decode goes through.
	DecodeNodeValue(pn ParsedNode, out any) error
	// EntityBodyJSON returns a node's kind-value mapping as canonical JSON, generically — with NO
	// concrete-kind Go type.
	EntityBodyJSON(pn ParsedNode) (json.RawMessage, error)
	// BuildFleetNode recursively builds a FleetNode from a fleet/resource node.
	BuildFleetNode(pn ParsedNode, t Threaded) (*FleetNode, error)
	// BuildResourceMemberChildren decodes pn's RESOURCE-MEMBER entity children into a
	// name→*FleetNode map via the SAME BuildFleetNode recursion — the SINGLE source of truth for
	// authored member-tree decode.
	BuildResourceMemberChildren(pn ParsedNode, t Threaded) (map[string]*FleetNode, error)
	// BuildFleetNodeInto builds pn into a FleetNode and registers it in acc's Fleet map — the
	// fallback for a recognized-but-not-yet-connected external deploy substrate word
	// (MaterializeSeams.BuildFleetEntity's implementation).
	BuildFleetNodeInto(pn ParsedNode, t Threaded, acc *MaterializedProject) error
	// IsDeployShape reports whether a substrate node is a DEPLOY (vs a standalone template).
	IsDeployShape(pn ParsedNode) bool
	// DecodeStandaloneTemplateJSON canonicalizes pn (a substrate TEMPLATE node) to the JSON the
	// host threads to the substrate plugin, GENERICALLY — with NO concrete-kind Go type.
	DecodeStandaloneTemplateJSON(pn ParsedNode, t Threaded) (json.RawMessage, error)
	// ResourceChildren returns pn's children whose discriminator is itself a resource/fleet kind
	// (the CUE-derived #ResourceKind vocab).
	ResourceChildren(pn ParsedNode) []ParsedNode

	// ValidateCandyManifestCUE validates a candy manifest: the whole-document #NodeDoc structural
	// gate, then the parsed+desugared entity-tree walk (ValidateNodeFormSteps).
	ValidateCandyManifestCUE(path string, data []byte, t Threaded, parser DocParser) error
	// ValidateNodeFormSteps parses a node-form document and validates EVERY entity's (and nested
	// sub-entity's) assembled body against its closed per-kind def — the step-typo gate for
	// candies, boxes, pods, deploys, and check beds alike.
	ValidateNodeFormSteps(path string, data []byte, t Threaded, parser DocParser) error

	// EnsureRepoDownloaded downloads repoPath@version if not already cached and returns the cache
	// path, auto-migrating it to the latest schema CalVer.
	EnsureRepoDownloaded(ctx context.Context, repoPath, version string) (string, error)
	// ResolveProjectRepo turns a --repo spec ("owner/repo", "owner/repo@ref", a host-qualified
	// path, or the "default" literal) into a local cache path a caller can chdir into. It is the
	// SAME clone-and-cache machinery EnsureRepoDownloaded drives, with the spec normalization and
	// default-branch resolution in front — pure spec vocabulary over one fetch, which is why it
	// belongs beside the fetch rather than in the kernel (K-wave 2 cone R1 relocated it out of
	// charly/main_repo.go).
	ResolveProjectRepo(ctx context.Context, repoSpec string) (string, error)
	// CanonicalRef resolves one `import:` ref — a local path, or
	// "@host/org/repo[/sub/path]:version" — relative to baseDir, into its stable dedup key and its
	// on-disk path, fetching a remote ref through the same clone-and-cache machinery. It is the
	// mechanism behind WalkSeams.ResolveRef; the host wires that seam to this method rather than
	// holding the resolution itself (K-wave 2 cone R1 relocated it out of charly/unified.go, the
	// same route ResolveProjectRepo took).
	CanonicalRef(ctx context.Context, ref, baseDir string) (key, path string, err error)
	// CollectRemoteRefsOpts collects all unique remote refs reachable from cfg's build/deploy
	// targets + layers' manifest depends/candy fields, grouped by (repoPath, version).
	CollectRemoteRefsOpts(ctx context.Context, cfg *Config, layers map[string]CandyReader, opts ResolveOpts) ([]RemoteDownload, error)
}

ProjectLoader is the swappable whole-project LOAD-ENTRY seam (#55 loader-keystone) — the terminal loader endpoint every command reaches to load a project's charly.yml. The loader plugin candy implements it (candy/plugin-loader, delegating to loaderkit.LoadUnified over loaderkit.LoadSeamsFromExecutor), and the host resolves the registered loader provider to it and drives LoadUnified THROUGH it — so charly core imports ONLY the dedicated spec module, never the loaderkit mechanism, to load its own config. The host supplies the registry-/host-coupled legs as a LoaderExecutor; the plugin owns the kind-blind orchestration. Typed (no wire envelope), the LOAD-ENTRY sibling of DocParser/ProjectWalker/CandyScanner/Materializer above — the compiled-in placement (the loader MUST always resolve; it is the config front-end) calls it directly, resolved at init() before the first load so there is no bootstrap cycle.

type ProjectTemplates

type ProjectTemplates struct {
	Local map[string]RawBody `yaml:"local,omitempty" json:"local,omitempty"`

	Kubernetes map[string]RawBody `yaml:"kubernetes,omitempty" json:"kubernetes,omitempty"`

	Pod map[string]RawBody `yaml:"pod,omitempty" json:"pod,omitempty"`

	VM map[string]RawBody `yaml:"vm,omitempty" json:"vm,omitempty"`

	Android map[string]RawBody `yaml:"android,omitempty" json:"android,omitempty"`
}

#ProjectTemplates — the bare pod:/vm:/local:/kubernetes:/android: template maps carried as OPAQUE payloads (the uf.Pod/VM/Local/Kubernetes/Android raw bytes, verbatim). The host projector stays KIND-BLIND — it copies the raw template bytes with NO concrete-kind decode (a kernel that read spec.Local/#Pod/… would violate the boundary law + trip TestNoConcreteKindInKernel). The CONSUMING PLUGINS (validate localtemplates, check-include pod/vm arms, status kubernetes/adb) decode a RawBody into the concrete spec kind type themselves — a plugin MAY know kinds, the kernel may not.

func (*ProjectTemplates) ByKind added in v0.2026217.301

func (t *ProjectTemplates) ByKind(kind string) map[string]RawBody

ByKind returns t's namespace-qualified template map for kind (nil for an unknown kind or a nil receiver). This is the ONE place ProjectTemplates' named fields fold back to a kind-keyed lookup — kept here (spec, the shared vocabulary) rather than in the kernel: a kernel caller that needs "the template map for THIS kind" stays genuinely kind-blind by keying on the string it already carries instead of switching on it itself (host_build_deploy_entity_resolve.go, W0).

type ProjectWalker

type ProjectWalker interface {
	WalkProject(rootDir string, rootData []byte, rootIdentity string, seams WalkSeams) (LoadedProject, error)
}

ProjectWalker is the swappable WHOLE-PROJECT WALK seam: the loader plugin candy implements it (candy/plugin-loader, delegating to loaderkit.Walk), and the host resolves the registered loader provider to it and calls it once per project load — so an alternative loader plugin serves a different walk mechanism by implementing this. Typed (no wire envelope) since the compiled-in placement passes live Go callbacks (WalkSeams) that cannot cross a JSON envelope. rootData is the (possibly bootstrap-transformed) root document bytes; rootIdentity seeds the namespace cycle-break with the root project's own repo identity.

type ProvidesConfig added in v0.2026210.808

type ProvidesConfig struct {
	Env []EnvProvideEntry `yaml:"env,omitempty" json:"env,omitempty"`

	MCP []MCPProvideEntry `yaml:"mcp,omitempty" json:"mcp,omitempty"`
}

#ProvidesConfig holds all resolved provides entries in charly.yml.

type RCARecord

type RCARecord struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	IncidentID UUIDv7 `yaml:"incident_id,omitempty" json:"incident_id"`

	State string `yaml:"state,omitempty" json:"state"`

	Findings []string `yaml:"findings,omitempty" json:"findings,omitempty"`

	RootCause string `yaml:"root_cause,omitempty" json:"root_cause,omitempty"`

	CompletedAt string `yaml:"completed_at,omitempty" json:"completed_at,omitempty"`
}

type RawBody

type RawBody = json.RawMessage

RawBody is an opaque JSON body the kernel stores and transports WITHOUT typing — a same-package alias for json.RawMessage so a CUE `@go(type=map[string]RawBody)` / `@go(type=RawBody)` override renders an opaque field/map in the generated types with no cross-package import (gengotypes would mis-import a qualified `json.RawMessage`). Moved here from the former hand-written deploy_wire.go (SDD conversion) — it is a scalar alias, not a wire struct, so it belongs beside the other scalar collapses in this file.

type Readiness

type Readiness struct {
	PollIntervalLocal Duration `yaml:"poll_interval_local,omitempty" json:"poll_interval_local,omitempty"`

	PollIntervalRemote Duration `yaml:"poll_interval_remote,omitempty" json:"poll_interval_remote,omitempty"`

	PollIntervalHeavy Duration `yaml:"poll_interval_heavy,omitempty" json:"poll_interval_heavy,omitempty"`

	PerAttempt Duration `yaml:"per_attempt,omitempty" json:"per_attempt,omitempty"`

	PerAttemptHeavy Duration `yaml:"per_attempt_heavy,omitempty" json:"per_attempt_heavy,omitempty"`

	NoProgress Duration `yaml:"no_progress,omitempty" json:"no_progress,omitempty"`

	AbsoluteCap Duration `yaml:"absolute_cap,omitempty" json:"absolute_cap,omitempty"`

	StopGrace Duration `yaml:"stop_grace,omitempty" json:"stop_grace,omitempty"`
}

ReadinessConfig (config.go / readiness_config.go) — the `defaults.readiness:` bounds for the unified pollUntil readiness primitive (poll.go). CLOSED. Every field is a Go time.ParseDuration string; absent → the named fallback const in poll.go. Defaults-only (reuses the BoxConfig type), but modeled here per the #Box completeness invariant in this file's header. The Resolve()-time ordering invariants (interval <= no_progress <= absolute_cap; poll_interval_local <= stop_grace <= absolute_cap) are cross-field and stay in Go (validateOrdering).

type ReadinessConfig

type ReadinessConfig = Readiness

--- box / deploy sub-types ---

type RebootStep added in v0.2026210.2308

type RebootStep struct {
	CandyName string
}

RebootStep requests a reboot of the deploy target after this candy's steps. It is emitted (last in the candy) when a candy declares `reboot: true` — the canonical case is a kernel-module candy (e.g. nvidia-open-dkms) whose module only loads on a fresh boot with nouveau blacklisted.

Only a rebootable venue acts on it: the external vm deploy reboots the guest over SSH (the host's RunHostStep RebootStep handler, gated on the rebootable-venue flag the vm substrate sets) and waits for the boot_id to change. OCITarget / PodDeployTarget have no machine to reboot at build time and skip it (the external kubernetes substrate does not consume the IR at all); the external local deploy refuses to reboot the host venue unattended (skip + note). Mirrors the ApkInstallStep "only one target executes" pattern.

func (*RebootStep) Kind added in v0.2026210.2308

func (s *RebootStep) Kind() StepKind

func (*RebootStep) RequiresGate added in v0.2026210.2308

func (s *RebootStep) RequiresGate() Gate

func (*RebootStep) Reverse added in v0.2026210.2308

func (s *RebootStep) Reverse() []ReverseOp

Reverse is empty — a reboot is not a persistent artifact to undo.

func (*RebootStep) Scope added in v0.2026210.2308

func (s *RebootStep) Scope() Scope

func (*RebootStep) Venue added in v0.2026210.2308

func (s *RebootStep) Venue() Venue

type RecoveryDecision

type RecoveryDecision struct {
	ID UUIDv7 `yaml:"id,omitempty" json:"id"`

	IncidentID UUIDv7 `yaml:"incident_id,omitempty" json:"incident_id"`

	RCAID UUIDv7 `yaml:"rca_id,omitempty" json:"rca_id,omitempty"`

	Action string `yaml:"action,omitempty" json:"action"`

	AuthorizedEmergencyAbort bool `yaml:"authorized_emergency_abort,omitempty" json:"authorized_emergency_abort,omitempty"`

	Params *RecoveryParams `yaml:"params,omitempty" json:"params,omitempty"`

	State string `yaml:"state,omitempty" json:"state"`

	DecidedAt string `yaml:"decided_at,omitempty" json:"decided_at"`

	AppliedAt string `yaml:"applied_at,omitempty" json:"applied_at,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type RecoveryParams

type RecoveryParams struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id,omitempty"`

	SessionID UUIDv7 `yaml:"session_id,omitempty" json:"session_id,omitempty"`

	Target *TargetSpec `yaml:"target,omitempty" json:"target,omitempty"`

	TerminalProfile *TerminalProfile `yaml:"terminal_profile,omitempty" json:"terminal_profile,omitempty"`

	Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`

	Prompt string `yaml:"prompt,omitempty" json:"prompt,omitempty"`

	Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"`

	Deployment string `yaml:"deployment,omitempty" json:"deployment,omitempty"`

	Note string `yaml:"note,omitempty" json:"note,omitempty"`
}

type RefsCollectSeams added in v0.2026217.301

type RefsCollectSeams struct {
	// Downloader is the registered remote-repo fetch backend (P7). The loader plugin reaches it as a
	// peer — InvokeProvider(class:"refs", word:"refs", OpResolve) — and wraps that dispatch in this
	// interface; a cache-miss download goes through it.
	Downloader RefsDownloader
	// MigrateCache brings a remote-repo cache's PROJECT files up to the head schema via the
	// compiled-in command:migrate plugin — registry-coupled (resolves ClassCommand "migrate" +
	// Invoke), so it stays a host-supplied callback.
	MigrateCache func(path string) error
	// ResolveLocal projects one opaque `kind:local` template body into a *ResolvedLocal via
	// candy/plugin-substrate's OpResolve leg — registry-coupled (Invoke), so it stays a
	// host-supplied callback.
	ResolveLocal func(body json.RawMessage) (*ResolvedLocal, error)
	// OverrideEnvValue is the raw CHARLY_REPO_OVERRIDE env value (RDD local-overrides) — the host
	// reads os.Getenv once; this mechanism never touches the env var NAME (spec/proc.RepoOverrideEnv,
	// #55 W3 B2-full — shared by charly core's plugin_loader.go and candy/plugin-check's
	// bed_session.go, both of which read/write it independently).
	OverrideEnvValue string
	// LatestTag resolves a repo's newest tag for a VERSION-LESS remote ref (the candy de-submodule
	// cutover, Phase 4: version-less refs resolve to the immutable latest tag, not the mutable
	// default branch). Registry-coupled (spec/refs.GitLatestTag shells out to git ls-remote), so it
	// stays a host-supplied callback — and a seam, so the mechanism is unit-testable offline.
	LatestTag func(repoURL string) (string, error)
}

RefsCollectSeams is the set of host-supplied callbacks EnsureRepoDownloaded/ CollectRemoteRefsOpts need for everything registry-coupled — the host builds this value and hands it to the ProjectLoader seam call; the mechanism never touches the provider registry directly (boundary law clause M: the resolve+invoke dispatch stays host-side, reached through these callbacks, exactly like WalkSeams/MaterializeSeams above).

K-wave 2 cone R1: the value is no longer built by charly core. candy/plugin-loader assembles it per call from the ctx-threaded executor (refs_seams.go) — core was only resolving three peers and reading one env var on the loader's behalf, which is a CALL, not a mechanism. The struct itself stays: it is still how the kind-blind loaderkit mechanism receives its registry-coupled legs, and keeping it a plain parameter is what lets the mechanism be unit-tested with stubs.

type RefsDownloadInput added in v0.2026218.138

type RefsDownloadInput struct {
	// repo_path is the bare repo coordinate (e.g. "github.com/opencharly/charly").
	RepoPath string `yaml:"repo_path,omitempty" json:"repo_path"`

	// version is the git tag / branch / sha to fetch. Never empty at this point: an empty authored
	// version is resolved to the repo's default branch before the request is built.
	Version string `yaml:"version,omitempty" json:"version"`
}

#RefsDownloadInput is the cache-MISS download request the loader plugin ships to the registered refs backend. The caller has already resolved local overrides and checked the cache, exactly as the pre-move host orchestration did — a backend only ever sees a genuine miss.

type RefsDownloadReply added in v0.2026218.138

type RefsDownloadReply struct {
	// dir is the absolute path of the populated cache directory.
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
}

#RefsDownloadReply carries the populated local cache tree the backend produced.

type RefsDownloader

type RefsDownloader interface {
	// Download fetches repoPath@version into the local repo cache and returns the cache path.
	// Called only on a cache MISS (the host checks IsRepoCached first).
	Download(repoPath, version string) (string, error)
}

RefsDownloader is the swappable remote-repo FETCH BACKEND seam (P7): the host dispatches every cache-miss download through a RefsDownloader; the DEFAULT (candy/plugin-refs, delegating to kit.DownloadRepo) fetches via git, and an alternative refs plugin can serve a different backend (OCI/S3-hosted candies) by registering a different RefsDownloader. Mirrors DocParser/ProjectWalker/ CandyScanner above: a typed interface a compiled-in plugin implements alongside its provider, so the host calls it in-proc with no wire envelope. Relocated from sdk/kit (FLOOR-SLIM axis-A mechanical batch) — the interface contract itself is kind-blind and registry-decoupled; the default git-fetch backend (kit.DefaultDownloader) stays in kit, wrapping the spec/refs.DownloadRepo git primitive (relocated to the spec/refs fabric slice, re-exported by kit for existing callers).

type RemoteDownload added in v0.2026213.1726

type RemoteDownload struct {
	RepoPath string
	Version  string
	Refs     []string // bare refs to import (e.g. "github.com/org/repo/candy/name")
}

RemoteDownload represents a unique (repo, version) pair to download in the remote-layer resolver's candy-scan fix-point, plus the bare refs to import from it. A pure data descriptor over strings, relocated to the dedicated spec module (#55 2b Class A) so charly's remote-resolver files (refs.go) reach it without importing loaderkit; loaderkit aliases it for the scan mechanism that produces it.

type RemoteImageResolveReply

type RemoteImageResolveReply struct {
	CacheDir string `yaml:"cache_dir,omitempty" json:"cache_dir,omitempty"`

	BoxName string `yaml:"box_name,omitempty" json:"box_name,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type RemoteImageResolveRequest

type RemoteImageResolveRequest struct {
	Ref string `yaml:"ref,omitempty" json:"ref"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`
}

#RemoteImageResolveRequest / #RemoteImageResolveReply — the "remote-image-resolve" HostBuild seam: clone/cache an @github.com/org/repo/box ref's source + return the cached dir + short box name. The host does ONLY the git clone/cache (EnsureRepoDownloaded, K1/B floor); the calling plugin resolves the registry pull ref itself via the K1 loader reverse legs (K1 loader wave — sheds deploykit.ResolveSpecBox from charly core; the former image_ref field is GONE).

type RenderedService

type RenderedService struct {
	UnitText string `yaml:"unit_text,omitempty" json:"unit_text,omitempty"`

	UnitPath string `yaml:"unit_path,omitempty" json:"unit_path,omitempty"`

	DropinText string `yaml:"dropin_text,omitempty" json:"dropin_text,omitempty"`

	DropinPath string `yaml:"dropin_path,omitempty" json:"dropin_path,omitempty"`
}

#RenderedService is the renderer output: the unit text, where it lands, and any drop-in for packaged-unit reuse.

type RepoBlock

type RepoBlock = map[string]any

--- open passthrough map (alias so []RepoBlock IS []map[string]any) ---

type RepoChangeStep added in v0.2026210.2308

type RepoChangeStep struct {
	Format    string // "rpm" | "deb" | "pac"
	File      string // absolute path of the repo file
	Content   string // rendered repo file body
	Checksum  string // sha256 of Content, for idempotency check at install
	CandyName string
}

RepoChangeStep records a repo config file mutation (e.g. adding rpmfusion-free.repo to /etc/yum.repos.d/). Distinct from SystemPackagesStep's PhasePrepare because the compiler often synthesizes these from `cmd:` tasks that happen to install a -release.rpm — we want them tracked separately so `charly fleet del` can reverse them precisely.

func (*RepoChangeStep) Kind added in v0.2026210.2308

func (s *RepoChangeStep) Kind() StepKind

func (*RepoChangeStep) RequiresGate added in v0.2026210.2308

func (s *RepoChangeStep) RequiresGate() Gate

func (*RepoChangeStep) Reverse added in v0.2026210.2308

func (s *RepoChangeStep) Reverse() []ReverseOp

func (*RepoChangeStep) Scope added in v0.2026210.2308

func (s *RepoChangeStep) Scope() Scope

func (*RepoChangeStep) Venue added in v0.2026210.2308

func (s *RepoChangeStep) Venue() Venue

type RepoSpec added in v0.2026210.2308

type RepoSpec struct {
	// Fields captured verbatim from the candy manifest. Different formats use
	// different subsets (rpm has url/id/key; deb has suite/components;
	// pac has url/keys). Carried as map so the template-rendered host
	// and container forms can pick what they need.
	Raw map[string]any
}

type ResolveOpts added in v0.2026213.1726

type ResolveOpts struct {
	IncludeDisabled      bool            // skip the `enabled: false` check
	IncludeDisabledNames map[string]bool // when non-empty, scope IncludeDisabled to these names only
	// RequestedBoxes are the explicit build targets (`charly box build <name>`). A qualified name
	// here (e.g. `charly.arch-builder`) is pulled into the resolved set even when it isn't reachable
	// as a base/builder of a root image — so a namespaced image can be an on-demand build target, not
	// only a transitive base. Bare names are ignored here (they resolve through the root loop).
	RequestedBoxes []string
	// ExtraCandyRefs are candy refs to collect IN ADDITION to the image/builder/kind:local-template
	// closure — specifically a DEPLOY's `add_candy:` candies. The image-closure walk (collectBox)
	// never reaches them, so a bed that add_candy's a host-side PLUGIN candy must pass its add_candy
	// refs here, or the plugin never enters the candy scan and loadProjectPlugins can't build/connect
	// it. NEVER read by the buildkit resolvers — consumed solely by the candy scan.
	ExtraCandyRefs []string
	// InitCfg is the project init: vocabulary (W9), threaded through so the candy scan can run the
	// cross-candy init-system host-completion pass (PopulateCandyInitSystem) BEFORE wrapping each
	// candy into the FINAL CandyReader. A caller that leaves this nil skips the pass (correct only for
	// a caller with no init-aware consumer downstream). NEVER read by the buildkit resolvers.
	InitCfg *InitConfig
	// DistroCfg / BuilderCfg are the project's build vocabulary (distro:/builder:), threaded through
	// so a resolve does not re-run the project load on every call (a caller with the triple, or a
	// multi-box loop, sets it once and skips the redundant reload; nil is byte-identical fallback).
	DistroCfg  *DistroConfig
	BuilderCfg *BuilderConfig
}

ResolveOpts carries the scan/load options threaded through the candy scan + project resolution.

func BoxResolveOpts added in v0.2026218.138

func BoxResolveOpts(boxes []string, includeDisabled bool) ResolveOpts

BoxResolveOpts builds the ResolveOpts that scope a generate/build to a set of explicitly-named boxes. It is the SINGLE source of the box-selection rule (R3) for `charly box build` and `charly box generate` alike: an empty slice means "all enabled boxes" (no scoping); a non-empty slice pins those names into the resolved set (RequestedBoxes) and, when includeDisabled is set, relaxes the `enabled: false` gate for exactly those names (IncludeDisabledNames) so the override never widens the working set globally. Callers pass boxes already run through buildkit.NormalizeBoxArgs.

It lived as charly's private `boxResolveOpts` until K-wave 2 cone R1 (A2). candy/plugin-build now builds the same value plugin-side to drive CollectRemoteRefsOpts itself, so the rule moves to the shared fabric module both sides import rather than being duplicated across the boundary — a pure ResolveOpts constructor over sibling spec types, pulling in no mechanism package.

func WithLocalRawRefs added in v0.2026218.138

func WithLocalRawRefs(opts ResolveOpts, localScanned map[string]ScannedCandy) ResolveOpts

WithLocalRawRefs returns opts with every local candy's RAW (pre-finalize) require:/candy: refs appended to ExtraCandyRefs. CollectRemoteRefsOpts's own "candy manifest require:/candy:" walk reads CandyView.Require/.IncludedCandy — the FINALIZED bare-string wire form (FinalizeCandyRefs strips a "@repo:vTAG" pin down to the bare graph-topology name; correct for its OWN consumers, ExpandCandy/ResolveCandyOrder, which are version-agnostic). Feeding that walk a wrapped view therefore leaves it structurally UNABLE to discover a local candy's pinned remote dep at all (a bare name never looks remote to IsRemoteCandyRef) — the confirmed root cause of a "depends: unknown candy" crash a live box/cachyos generate surfaced (a local candy's require: pins a remote plugin candy). So the raw pre-finalize refs (still carrying the full pin, from ScannedCandy.Refs) are harvested here and fed in as ExtraCandyRefs — the SAME mechanism a deploy's add_candy: already uses to reach a ref no base/builder/require edge would otherwise surface. A local (non-remote) ref is a harmless no-op (IsRemoteCandyRef gates it).

Relocated from charly/layers.go in K-wave 2 cone R1 (A2) for the same reason as BoxResolveOpts: candy/plugin-build's own CollectRemoteRefsOpts call needs the identical augmentation, and a second copy across the module boundary is the R3 duplicate this program removes.

func (ResolveOpts) ShouldIncludeDisabled added in v0.2026213.1726

func (opts ResolveOpts) ShouldIncludeDisabled(name string) bool

ShouldIncludeDisabled reports whether name's disabled gate should be bypassed under opts. Centralizes the IncludeDisabled + IncludeDisabledNames interaction so call sites stay simple.

type ResolvedAndroid

type ResolvedAndroid struct {
	Serial string `yaml:"serial,omitempty" json:"serial,omitempty"`

	Device string `yaml:"device,omitempty" json:"device,omitempty"`

	ApiLevel int `yaml:"api_level,omitempty" json:"api_level,omitempty"`

	GoogleAccount *GoogleAccount `yaml:"google_account,omitempty" json:"google_account,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Adb *AdbEndpoint `yaml:"adb,omitempty" json:"adb,omitempty"`

	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#ResolvedAndroid is the resolve-to-envelope form of an `android:` device template.

func (*ResolvedAndroid) EffectiveSerial

func (a *ResolvedAndroid) EffectiveSerial() string

EffectiveSerial returns the device serial, defaulting to the emulator serial.

func (*ResolvedAndroid) IsEndpoint

func (a *ResolvedAndroid) IsEndpoint() bool

IsEndpoint reports whether the resolved device targets a remote adb endpoint.

type ResolvedBox added in v0.2026210.1533

type ResolvedBox struct {
	Name string `yaml:"name,omitempty" json:"name"`

	// version is the authored per-entity CalVer (the box config `version:`); optional.
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	// effective_version is the content-derived identity emitted as the ai.opencharly.version
	// label: the dedicated version if set, else the highest candy version across the full
	// chain. Stable across builds when no candy changed.
	EffectiveVersion string `yaml:"effective_version,omitempty" json:"effective_version,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	Info string `yaml:"info,omitempty" json:"info,omitempty"`

	CheckLevel string `yaml:"check_level,omitempty" json:"check_level,omitempty"`

	Base string `yaml:"base,omitempty" json:"base"`

	// from mirrors BoxConfig.From after resolution. When non-empty (e.g. "builder:pacstrap"),
	// the generator emits FROM scratch + ADD <staged-rootfs.tar.gz> instead of FROM <base>.
	From string `yaml:"from,omitempty" json:"from,omitempty"`

	BootstrapBuilderImage string `yaml:"bootstrap_builder_image,omitempty" json:"bootstrap_builder_image,omitempty"`

	Platforms []string `yaml:"platforms,omitempty" json:"platforms"`

	Tag string `yaml:"tag,omitempty" json:"tag"`

	Registry string `yaml:"registry,omitempty" json:"registry"`

	Pkg string `yaml:"pkg,omitempty" json:"pkg"`

	Distro []string `yaml:"distro,omitempty" json:"distro"`

	BuildFormats []string `yaml:"build_formats,omitempty" json:"build_formats"`

	Tags []string `yaml:"tags,omitempty" json:"tags"`

	Candy []string `yaml:"candy,omitempty" json:"candy"`

	// User configuration.
	User string `yaml:"user,omitempty" json:"user"`

	UID int `yaml:"uid,omitempty" json:"uid"`

	GID int `yaml:"gid,omitempty" json:"gid"`

	Home string `yaml:"home,omitempty" json:"home"`

	// user_adopted is true when the resolved user came from the distro's base_user
	// declaration rather than being created by the bootstrap.
	UserAdopted bool `yaml:"user_adopted,omitempty" json:"user_adopted"`

	// Merge configuration — layer merge settings (nil means use CLI defaults).
	Merge *BoxMerge `yaml:"merge,omitempty" json:"merge,omitempty"`

	// Builder configuration (resolved: image -> base image -> defaults -> {}).
	Builder BuilderMap `yaml:"builder,omitempty" json:"builder"`

	// Builder capability declaration (image-specific, not inherited).
	BuilderCapabilities []string `yaml:"builder_capabilities,omitempty" json:"builder_capabilities"`

	Auto bool `yaml:"auto,omitempty" json:"auto"`

	// Container network mode (e.g. "host", "none") — declaration of required/recommended
	// network mode. Deployment overrides via MergeDeployOntoMetadata.
	Network string `yaml:"network,omitempty" json:"network"`

	DataImage bool `yaml:"data_image,omitempty" json:"data_image"`

	// OCI entrypoint/cmd baked into the image's OCI config (final Containerfile
	// ENTRYPOINT/CMD). Empty by default — normal images carry no baked command and
	// the deploy-time init injects it; declared only when something spawns the image
	// directly from its OCI config (e.g. an embedded controller spawning child
	// agents) with no charly deploy in the loop.
	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"`

	// Derived fields.
	IsExternalBase bool `yaml:"is_external_base,omitempty" json:"is_external_base"`

	FullTag string `yaml:"full_tag,omitempty" json:"full_tag"`
}

#ResolvedBox — a fully resolved box configuration, the wire-clean subset. EXCLUDED here (both stay on the buildkit.ResolvedBox embedding wrapper, as hand `yaml:"-" json:"-"` fields — the gengotypes CANNOT-table's "json:"-" keep-in-Go, drop-from-wire" exception, spike-verified: no CUE construct emits a suppressed-from-marshal field):

  • the three host-render-cache VOCABULARY pointers DistroConfig/DistroDef/BuilderConfig (+ InitSystem/InitDef) — buildkit-owned types spec must never import;
  • the four RENDER-cache fields CandyCaps/BakedMetadata/RenderCandyOrder/ActiveInits — `yaml:"-" json:"-"` on ResolvedBox itself (the SEPARATE ResolvedBoxView wire type carries the actual wire copies of this same data, via NewSpecResolvedBox/ProjectResolvedBox).

Fields that carried NO json tag on the former hand struct (always emitted, no omitempty) are REQUIRED (`!`) here; fields that already carried `,omitempty` are OPTIONAL (`?`).

func (*ResolvedBox) SupportsBuild added in v0.2026210.1533

func (img *ResolvedBox) SupportsBuild(format string) bool

SupportsBuild returns true if this image has the given build format.

func (*ResolvedBox) SupportsTag added in v0.2026210.1533

func (img *ResolvedBox) SupportsTag(tag string) bool

SupportsTag returns true if this image has the given tag. Tags include format (rpm, deb, pac), distro (fedora, arch), version (fedora:43), and the implicit "all".

type ResolvedBoxView

type ResolvedBoxView struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	EffectiveVersion string `yaml:"effective_version,omitempty" json:"effective_version,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	Info string `yaml:"info,omitempty" json:"info,omitempty"`

	CheckLevel string `yaml:"check_level,omitempty" json:"check_level,omitempty"`

	Base string `yaml:"base,omitempty" json:"base,omitempty"`

	From string `yaml:"from,omitempty" json:"from,omitempty"`

	BootstrapBuilderImage string `yaml:"bootstrap_builder_image,omitempty" json:"bootstrap_builder_image,omitempty"`

	Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`

	Pkg string `yaml:"pkg,omitempty" json:"pkg,omitempty"`

	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`

	BuildFormats []string `yaml:"build_formats,omitempty" json:"build_formats,omitempty"`

	Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`

	Candy []string `yaml:"candy,omitempty" json:"candy,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	UID int64 `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int64 `yaml:"gid,omitempty" json:"gid,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	UserAdopted bool `yaml:"user_adopted,omitempty" json:"user_adopted,omitempty"`

	Merge *BoxMerge `yaml:"merge,omitempty" json:"merge,omitempty"`

	Builder map[string]string `yaml:"builder,omitempty" json:"builder,omitempty"`

	BuilderCapabilities []string `yaml:"builder_capabilities,omitempty" json:"builder_capabilities,omitempty"`

	Auto bool `yaml:"auto,omitempty" json:"auto,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	DataImage bool `yaml:"data_image,omitempty" json:"data_image,omitempty"`

	// OCI entrypoint/cmd baked into the image's OCI config (final Containerfile
	// ENTRYPOINT/CMD) — carried on the resolved view so the plugin-build resolve
	// leg (NewSpecResolvedBox) can re-emit them without the live *Candy graph.
	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	Cmd []string `yaml:"cmd,omitempty" json:"cmd,omitempty"`

	IsExternalBase bool `yaml:"is_external_base,omitempty" json:"is_external_base,omitempty"`

	FullTag string `yaml:"full_tag,omitempty" json:"full_tag,omitempty"`

	// box-AGGREGATES — the cross-candy effective values `charly box inspect --format
	// ports|volumes|aliases|engine` prints (the host projector runs CollectBoxPorts /
	// CollectBoxVolume / CollectBoxAlias / ResolveBoxEngine into these). engine is the raw
	// resolver result ("" → the command body renders "(global default)").
	Ports []string `yaml:"ports,omitempty" json:"ports,omitempty"`

	Volumes []ResolvedVolumeMount `yaml:"volumes,omitempty" json:"volumes,omitempty"`

	Aliases []CandyAlias `yaml:"aliases,omitempty" json:"aliases,omitempty"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	// box-AUTHORED surfaces the validate ENGINE checks (task #60): the box's OWN authored `plan:`
	// (validateOps box-arm — every Op validated for authoring errors) and `alias:` (validateAliases
	// box-arm — name char-set + cross-entry dedup). Distinct from the resolved `aliases` AGGREGATE
	// above (cross-candy CollectBoxAlias) — these are the box entity's raw authored entries the plugin
	// re-validates, so the whole validateOps/validateAliases move to plugin-box (ruling-a: grow the
	// envelope rather than keep the R-rule in core).
	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	AuthoredAliases []BoxAlias `yaml:"authored_aliases,omitempty" json:"authored_aliases,omitempty"`

	// box-AUTHORED deploy-overlay surfaces ExportAllBox reads (K5-Unit-1, the #67 keystone): the
	// box entity's OWN authored env / env_file / security / network / raw description — the fields
	// `charly fleet export --all` projects into a FleetConfig so the deploy-state model can be
	// built from the RESOLVED-PROJECT ENVELOPE, not the live *Config graph. description is the RAW
	// authored string (distinct from info above, which is its descriptionInfo first-line summary);
	// env/env_file/security are the box-authored deploy-overlay defaults. network is already carried
	// above; these three + description are the additions. Filled by projectBoxAggregates from
	// cfg.BoxConfig(name), alongside plan/authored_aliases.
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	// build-render label carriers (#67, the build_resolve RENDER-leg death). The host projector runs
	// the label collectors + caps + init resolve + skill probe + status into a fully-baked #BakedLabelSet
	// (the EXACT wire data writeLabels emits) so the deploykit render's WriteLabels — relocated out of
	// core — FORMATS it byte-for-byte WITHOUT the live *Candy graph. #BakedLabelSet is the BUILD-side
	// wire-form carrier (env map, volume LabelVolumeEntry, +init_label_key/oci_labels), distinct from
	// #BoxMetadata (the deploy-side hub). caps carries the aggregated candy capability surface the
	// render reads beyond the baked labels (the two render-gating booleans; oci_labels also ride the
	// baked set for the formatter). Both filled ONLY in the build-render projection; empty for the
	// resolved-project / validate path (which never renders), so that path stays lean.
	BakedMetadata *BakedLabelSet `yaml:"baked_metadata,omitempty" json:"baked_metadata,omitempty"`

	Caps *AggregatedCandyCapsView `yaml:"caps,omitempty" json:"caps,omitempty"`

	// build-RENDER init/candy-order caches (#67, the build_resolve RENDER-leg death). The host
	// render-prep computes these over the live *Candy/*Config graph (globalOrderForBox,
	// AggregateCandyCapabilities, ActiveInit, ResolveInitSystem) and either sets them directly on
	// the live ResolvedBox (the parity-test live path) or carries them through this envelope (the
	// plugin-build drive path, re-attached by NewSpecResolvedBox) so the deploykit render reads
	// RESOLVED caches WITHOUT the live graph. Filled ONLY in the build-render projection; empty for
	// the resolved-project / validate path. render_candy_order is the per-box cache-optimal candy
	// sequence; init_system/init_def the active init; active_inits the full active-init map
	// (EmitInitFragmentStages + EmitInitAssembly read it).
	RenderCandyOrder []string `yaml:"render_candy_order,omitempty" json:"render_candy_order,omitempty"`

	InitSystem string `yaml:"init_system,omitempty" json:"init_system,omitempty"`

	InitDef *ResolvedInit `yaml:"init_def,omitempty" json:"init_def,omitempty"`

	ActiveInits map[string]*ResolvedInit `yaml:"active_inits,omitempty" json:"active_inits,omitempty"`
}

#ResolvedBoxView — the resolved box METADATA a consumer reads: EXACTLY the non-json:"-" fields of buildkit.ResolvedBox, in declaration order, so this view is the wire-safe half of what `charly box inspect` already serializes (json.MarshalIndent(*ResolvedBox) — which never reaches the 6 json:"-" compute-cache pointers). Field order mirrors ResolvedBox so a future inspect relocation projects into this view byte-faithfully. Every field optional (a projection envelope); name is the stable key.

type ResolvedDistro

type ResolvedDistro struct {
	Inherits string `yaml:"inherits,omitempty" json:"inherits,omitempty"`

	InheritPackages bool `yaml:"inherit_packages,omitempty" json:"inherit_packages,omitempty"`

	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Bootstrap Bootstrap `yaml:"bootstrap,omitempty" json:"bootstrap,omitempty"`

	Workarounds []string `yaml:"workaround,omitempty" json:"workaround,omitempty"`

	Format map[string]*Format `yaml:"format,omitempty" json:"format,omitempty"`

	BaseUser *BaseUser `yaml:"base_user,omitempty" json:"base_user,omitempty"`

	Pacstrap *Pacstrap `yaml:"pacstrap,omitempty" json:"pacstrap,omitempty"`

	Debootstrap *Debootstrap `yaml:"debootstrap,omitempty" json:"debootstrap,omitempty"`

	AlpineBootstrap *AlpineBootstrap `yaml:"alpine_bootstrap,omitempty" json:"alpine_bootstrap,omitempty"`

	Bootloader *Bootloader `yaml:"bootloader,omitempty" json:"bootloader,omitempty"`

	DiskLayout *DiskLayout `yaml:"disk_layout,omitempty" json:"disk_layout,omitempty"`

	Dnf *Dnf `yaml:"dnf,omitempty" json:"dnf,omitempty"`

	Installer *DistroInstaller `yaml:"installer,omitempty" json:"installer,omitempty"`

	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

--- resolve-to-envelope wire type (Cutover M, the long pole; SDD conversion, per the standing operator directive: a hand-written wire struct not yet CUE-sourced is conversion-in-progress, never a sanctioned exception). candy/plugin-distro resolves an authored `distro:` build-vocabulary entity into a ResolvedDistro the kernel's build engine consumes without importing the concrete spec.Distro. Written out explicitly (not embedding #Distro) so every field's required/optional state is independently auditable against the former hand type. The host keeps RenderTemplate + the cache-mount vocab (per the plan); the plugin owns the distro KNOWLEDGE (schema/typed shape/validation). PrimaryFormat()/LocalPkgFormat() are pure Go METHODS — CUE cannot express them — and stay hand-written in spec/distro_methods.go (mirrors Op.Kind() in spec/charly_methods.go: a method, not a type).

func (*ResolvedDistro) LocalPkgFormat

func (d *ResolvedDistro) LocalPkgFormat(primaryFormat string) (string, *LocalPkg)

LocalPkgFormat picks the format whose local_pkg block drives the charly package INSTALL (the download leg): the caller's primary format, then PrimaryFormat, then any localpkg-capable format.

func (*ResolvedDistro) PrimaryFormat

func (d *ResolvedDistro) PrimaryFormat() string

PrimaryFormat returns the distro's primary (non-secondary) build format — the deterministic first non-secondary Format name.

type ResolvedGpuSelector

type ResolvedGpuSelector struct {
	Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty"`
}

type ResolvedInit

type ResolvedInit struct {
	CandyFields []string `yaml:"candy_field,omitempty" json:"candy_field,omitempty"`

	CandyFiles []string `yaml:"candy_file,omitempty" json:"candy_file,omitempty"`

	DependsCandy string `yaml:"depends_candy,omitempty" json:"depends_candy,omitempty"`

	RequiresCapability []string `yaml:"requires_capability,omitempty" json:"requires_capability,omitempty"`

	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	HeaderFile string `yaml:"header_file,omitempty" json:"header_file,omitempty"`

	FragmentDir string `yaml:"fragment_dir,omitempty" json:"fragment_dir,omitempty"`

	RelayTemplate string `yaml:"relay_template,omitempty" json:"relay_template,omitempty"`

	StageName string `yaml:"stage_name,omitempty" json:"stage_name,omitempty"`

	StageHeaderCopy string `yaml:"stage_header_copy,omitempty" json:"stage_header_copy,omitempty"`

	StageFragmentCopy string `yaml:"stage_fragment_copy,omitempty" json:"stage_fragment_copy,omitempty"`

	AssemblyTemplate string `yaml:"assembly_template,omitempty" json:"assembly_template,omitempty"`

	SystemEnableTemplate string `yaml:"system_enable_template,omitempty" json:"system_enable_template,omitempty"`

	PostAssemblyTemplate string `yaml:"post_assembly_template,omitempty" json:"post_assembly_template,omitempty"`

	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`

	FallbackEntrypoint []string `yaml:"fallback_entrypoint,omitempty" json:"fallback_entrypoint,omitempty"`

	ManagementTool string `yaml:"management_tool,omitempty" json:"management_tool,omitempty"`

	ManagementCommands map[string]string `yaml:"management_command,omitempty" json:"management_command,omitempty"`

	LabelKey string `yaml:"label_key,omitempty" json:"label_key,omitempty"`

	ServiceSchema *InitServiceSchema `yaml:"service_schema,omitempty" json:"service_schema,omitempty"`

	// Raw is the opaque init body — threaded to the service-render leg (leg 1).
	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#ResolvedInit is the resolve-to-envelope form of an init system the kernel consumes for stage/assembly emission, capability labels, and the entrypoint contract (legs 2–4 of the init de-type). It carries the init system's build + runtime VALUES + templates the host reads generically — the kernel never imports the concrete spec.Init. Raw is the opaque init body threaded to the plugin's service-render leg (leg 1). candy/plugin-init produces it from spec.Init.

type ResolvedKubernetes added in v0.2026223.1018

type ResolvedKubernetes struct {
	KubeconfigContext string `yaml:"kubeconfig_context,omitempty" json:"kubeconfig_context,omitempty"`

	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#ResolvedKubernetes is the resolve-to-envelope form of a `kubernetes:` cluster template. The kernel reads only KubeconfigContext (the deploy preresolver); the full cluster model rides opaquely in Raw and is decoded by candy/plugin-k8sgen, never the kernel.

type ResolvedLocal

type ResolvedLocal struct {
	Candy []CandyRef `yaml:"candy,omitempty" json:"candy,omitempty"`

	InstallOpts *InstallOpts `yaml:"install_opts,omitempty" json:"install_opts,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	// raw is the opaque authored body (the kernel passes it back unread where
	// a verbatim template is needed).
	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#ResolvedLocal is the resolve-to-envelope form of a `local:` template — a candy-stack applied to a host. The kernel reads it (candy stack / install opts / plan), never spec.Local.

type ResolvedPod

type ResolvedPod struct {
	Box CandyRef `yaml:"box,omitempty" json:"box,omitempty"`

	Sidecar []PodSidecar `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`

	Secret []DeploySecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	EnvDefaults map[string]string `yaml:"env_default,omitempty" json:"env_default,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#ResolvedPod is the resolve-to-envelope form of a `pod:` template — a box + sidecar + plan fleet. The kernel reads it (currently the include-spliced Plan), never spec.Pod.

type ResolvedProject

type ResolvedProject struct {
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	Boxes map[string]ResolvedBoxView `yaml:"boxes,omitempty" json:"boxes,omitempty"`

	Candies map[string]CandyView `yaml:"candies,omitempty" json:"candies,omitempty"`

	// candy_models — the serializable candy BUILD models (validate, the plan-include splicer, K3-D)
	// keyed by name, distinct from candies (identity/graph). See candymodel.cue.
	CandyModels map[string]CandyModel `yaml:"candy_models,omitempty" json:"candy_models,omitempty"`

	Deploy map[string]*Deploy `yaml:"deploy,omitempty" json:"deploy,omitempty"`

	// build vocabulary (the validate ENGINE consumer): distro/init PIN the hand wire structs
	// (spec.ResolvedDistro/ResolvedInit — the distro/init de-type OpResolve envelopes, no #CUE def);
	// #Builder is a clean def. Pointer maps for parity with DistroConfig/BuilderConfig/InitConfig.Init.
	Distro map[string]*ResolvedDistro `yaml:"distro,omitempty" json:"distro,omitempty"`

	Builder map[string]*Builder `yaml:"builder,omitempty" json:"builder,omitempty"`

	Init map[string]*ResolvedInit `yaml:"init,omitempty" json:"init,omitempty"`

	// kind templates (validate localtemplates + check-include pod/vm arms + status kubernetes/adb enumeration).
	Templates *ProjectTemplates `yaml:"templates,omitempty" json:"templates,omitempty"`

	// kind:agent catalog (the harness AI-CLI pick — plugin-check reads it off this envelope; charly feature list-agent).
	AgentBodies map[string]RawBody `yaml:"agent_bodies,omitempty" json:"agent_bodies,omitempty"`

	// build order + auto-intermediates (charly box list targets).
	BuildTargets []BuildTarget `yaml:"build_targets,omitempty" json:"build_targets,omitempty"`

	// validate D-data word-sets (task #60, ruling: host projects the registry facts into the envelope
	// so the validate ENGINE moves to the plugin WITHOUT the plugin ever dialing the host registry).
	// The host fills these in the validate-project path only (empty for the resolved-project path);
	// clause-D kind/word-recognition DATA consulted BY WORD, never a per-kind branch.
	//
	//	provider_capabilities — every compiled-in provider as "<class>:<word>" (validatePluginCandy
	//	  checks a `source: builtin` candy's declared providers are actually compiled in).
	//	act_capable_verbs — the plugin WORDS whose act form has a build/deploy install path (the host
	//	  type-asserts ProvisionActor/TypedStepProvider/BuildEmitter + connected/declared externals +
	//	  command, exactly as core's opActsInBuildDeploy does), so validateCheck's act-form rule keeps
	//	  builtin rejection behaviour without the registry.
	ProviderCapabilities []string `yaml:"provider_capabilities,omitempty" json:"provider_capabilities,omitempty"`

	ActCapableVerbs []string `yaml:"act_capable_verbs,omitempty" json:"act_capable_verbs,omitempty"`

	// box_plans — the include-ready FLATTENED acceptance plan per box (the `include: box:<name>`
	// arm of the plan-composition splicer). The host projector runs the SAME base-chain walk
	// CollectDescriptions uses (candy-chain bakeable steps + the box-level bakeable plan) and
	// flattens the three sections into one ordered []Step, keyed by the QUALIFIED box name
	// (namespaced boxes like `fedora.jupyter` included) — the exact result the former in-core
	// box arm produced. A plugin cannot recompute it (base-chain + candy-order + bakeable filter
	// are host resolve Mechanisms over the runtime Candy), so the resolve engine ships it. Only
	// boxes with a non-empty flattened plan appear.
	BoxPlans map[string][]Step `yaml:"box_plans,omitempty" json:"box_plans,omitempty"`

	// global_order — the popularity-weighted GLOBAL candy order (GlobalCandyOrder) the
	// build RENDER reads (deploykit Generator.GlobalOrderForBox → per-box candy sequence for
	// cache-optimal layering). A host resolve Mechanism over the runtime Candy graph, so the
	// resolve engine ships it (the plugin-side render can't recompute it). Filled in the
	// build-render projection (#67); empty for the resolved-project/validate path.
	GlobalOrder []string `yaml:"global_order,omitempty" json:"global_order,omitempty"`

	// externalized_builders — the registry D-FACT: which detection-builder WORDS
	// (pixi/npm/aur/cargo) are externalized (their inline render crosses the OpResolve seam vs
	// an in-core vocabulary). The render selects the branch by word (deploykit
	// DetectExternalizedBuilders + candy_steps builder arm). clause-D word-recognition DATA
	// consulted BY WORD; filled in the build-render projection (#67).
	ExternalizedBuilders map[string]bool `yaml:"externalized_builders,omitempty" json:"externalized_builders,omitempty"`

	// resources — the resolved `resource:` kind entities (K1-unblock wave 1), keyed by name,
	// mirroring `deploy` byte-for-byte: the host decodes each project resource:` entity via the
	// SAME kind-blind resource-kind plugin resolve it always used (never a kernel decode of the
	// concrete #Resource shape — clause-D/M unaffected), so this is a plain data projection, not a
	// new engine. Consumer: the resource arbiter (candy/plugin-preempt), which previously reached
	// this ONLY via the bespoke ExecutorService.HostArbiter "resources" seam
	// (charly/arbiter_host.go) — the last of its 2 genuinely-LoadUnified-coupled host seams. Reading
	// it off THIS envelope instead retires that seam, leaving `resources` (this field) as the single
	// source both the arbiter plugin and any other resolved-project consumer share (R3).
	Resources map[string]*ResolvedResource `yaml:"resources,omitempty" json:"resources,omitempty"`

	// primaries — the plugin-verb PRIMARY-field D-fact: a plugin verb WORD → its declared primary
	// input field (`cdp` → `method`, `file` → `file`), the SAME registry-derived kind-recognition
	// DATA the LOAD path already snapshots as spec.Threaded.Primaries (loaderThreaded), carried here
	// SAVE-side too. The deploy-state WRITER's plan RESUGAR (deploy_nodeform.go's resugarPlan —
	// collapsing an internal plugin/plugin_input pair back to the authored `<word>: <input>` sugar)
	// needs this map to collapse a single-primary input to its scalar shorthand; carrying it on the
	// envelope lets a plugin holding this projection resugar a plan WITHOUT dialing the host provider
	// registry (the K4 keystone that unblocks moving the deploy-state marshal plugin-side). clause-D
	// word-recognition DATA consulted BY WORD, never a per-kind branch; filled on every resolve path.
	Primaries map[string]string `yaml:"primaries,omitempty" json:"primaries,omitempty"`
}

#ResolvedProject — the whole resolved projection: the schema version, the resolved boxes keyed by name, the resolved candy graph keyed by name, and the deploy tree (uf.Fleet verbatim — already map[string]spec.Deploy). The deploy map is @go-pinned to a pointer map so `cue exp gengotypes` generates map[string]*Deploy (recursive tree, faithful). provides/sidecar are additive later members of this same envelope (added by the consumer unit that first needs them).

type ResolvedProjectRequest

type ResolvedProjectRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	IncludeDisabled bool `yaml:"include_disabled,omitempty" json:"include_disabled,omitempty"`

	// Check commands resolving a distro-submodule project must see the parent
	// superproject's in-development candies before bed classification. The host
	// applies the same local override used by the later R10 session only for the
	// duration of this projection.
	LocalSuperproject bool `yaml:"local_superproject,omitempty" json:"local_superproject,omitempty"`

	// A caller compiling against an `add_candy:`/`--add-candy` ref (a candy the project's own
	// image-closure walk never reaches — the reachability-scoped remote-ref collection is
	// deliberately narrower than "every candy anywhere," R1) widens the scan the SAME way
	// ScanAllCandyWithConfigOpts' ResolveOpts.ExtraCandyRefs already does for a check bed's own
	// add_candy: (deploy_add_shared.go's deployNodePluginContext) — without this, the compile
	// plugin's OWN independent resolved-project re-fetch (candy/plugin-fleet's compile.go)
	// never discovers a remote add-candy ref the HOST's separate scanCandiesForRef call already
	// pulled in via its own synthetic-augmented scan, and BuildDeployPlan fails "candy not in
	// resolved-project envelope" (RCA'd K1-alpha regression, check-addcandy-pod/check-stepkind-
	// emit-pod).
	ExtraCandyRefs []string `yaml:"extra_candy_refs,omitempty" json:"extra_candy_refs,omitempty"`

	// requested_boxes — the explicit build/generate targets (`charly box generate <name>`),
	// buildkit.NormalizeBoxArgs-normalized (task #17 fix). The reachability-scoped remote-ref
	// COLLECTION walk (loaderkit.CollectRemoteRefsOpts) only follows base/builder edges from
	// ROOT-owned images by default — a namespace-qualified on-demand target
	// (`charly box generate fedora.check-pod`) that is not itself a base/builder of any root
	// image is otherwise never visited, so its own remote candy refs are silently never
	// collected/fetched, and the later candy-order RESOLVE step then fails "unknown candy" for a
	// ref the fetch step skipped. Mirrors spec.ResolveOpts.RequestedBoxes, which already pulls a
	// qualified on-demand target into buildkit.ResolveAllBox's RESOLVE set the identical way —
	// this closes the matching gap on the COLLECT half.
	//
	// The `boxes` field is DELETED (K-wave 2 cone R1). It carried the plugin-resolved box set to the
	// host's `buildengine-prep` leg, whose only job was seeding the render-seam floor's Generator
	// cache. That seam is gone — every render seam peer-dispatches now — so the leg, the cache, and
	// this field all lost their single consumer together, along with deploykit's host-side
	// ResolveAllSpecBoxes/WrapSpecBoxes/SpecBoxes bridge.
	RequestedBoxes []string `yaml:"requested_boxes,omitempty" json:"requested_boxes,omitempty"`
}

#ResolvedProjectRequest — the `resolved-project` HostBuild request: which project dir to resolve (empty = the host's cwd) and whether to include enabled:false boxes. The reply is #ResolvedProject.

type ResolvedResource

type ResolvedResource struct {
	Gpu *ResolvedGpuSelector `yaml:"gpu,omitempty" json:"gpu,omitempty"`
}

type ResolvedSidecar

type ResolvedSidecar struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	Secret []ResolvedSidecarSecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	Volume []ResolvedSidecarVolume `yaml:"volume,omitempty" json:"volume,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`
}

#ResolvedSidecar is a fully-resolved co-deployed container the host consumes for quadlet generation — the resolve-to-envelope form of a sidecar (NO Sidecar / SidecarDef fields survive; the plugin already merged + resolved everything).

type ResolvedSidecarSecret

type ResolvedSidecarSecret struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Env string `yaml:"env,omitempty" json:"env,omitempty"`

	HostEnv string `yaml:"host_env,omitempty" json:"host_env,omitempty"`

	SecretName string `yaml:"secret_name,omitempty" json:"secret_name,omitempty"`
}

#ResolvedSidecarSecret is a resolved sidecar secret (the sidecar-scoped subset of the host's CollectedSecret): the podman secret name + the container/host env-var names + the original manifest secret name.

type ResolvedSidecarVolume

type ResolvedSidecarVolume struct {
	VolumeName string `yaml:"volume_name,omitempty" json:"volume_name"`

	ContainerPath string `yaml:"container_path,omitempty" json:"container_path"`
}

#ResolvedSidecarVolume is a resolved sidecar volume: the charly-scoped volume name + its container mount path.

type ResolvedVm

type ResolvedVm struct {
	Source VmSource `yaml:"source,omitempty" json:"source"`

	DiskSize string `yaml:"disk_size,omitempty" json:"disk_size,omitempty"`

	Ram string `yaml:"ram,omitempty" json:"ram,omitempty"`

	Cpus int `yaml:"cpu,omitempty" json:"cpu,omitempty"`

	Machine string `yaml:"machine,omitempty" json:"machine,omitempty"`

	Firmware string `yaml:"firmware,omitempty" json:"firmware"`

	Backend string `yaml:"backend,omitempty" json:"backend"`

	Autostart bool `yaml:"autostart,omitempty" json:"autostart"`

	Network *VmNetwork `yaml:"network,omitempty" json:"network,omitempty"`

	SSH *VmSsh `yaml:"ssh,omitempty" json:"ssh,omitempty"`

	CloudInit *VmCloudInit `yaml:"cloud_init,omitempty" json:"cloud_init,omitempty"`

	Libvirt *LibvirtDomain `yaml:"libvirt,omitempty" json:"libvirt,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Snapshots []VmSnapshot `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`

	Raw RawBody `yaml:"raw,omitempty" json:"raw,omitempty"`
}

--- resolve-to-envelope wire type (Cutover L; SDD conversion, per the standing operator directive: a hand-written wire struct not yet CUE-sourced is conversion-in-progress, never a sanctioned exception). ResolvedVm mirrors #Vm's fields — written out explicitly rather than embedding #Vm, since the resolved envelope carries PLAIN post-default scalars, not #Vm's own enum/default machinery (firmware/backend/autostart are plain string/bool here, never the `*"bios"|...` disjunction). candy/plugin-substrate resolves an authored `vm:` template into this envelope; the kernel's vm build/deploy consumers read it without importing the concrete spec.Vm.

type ResolvedVolumeMount

type ResolvedVolumeMount struct {
	VolumeName string `yaml:"volume_name,omitempty" json:"volume_name,omitempty"`

	ContainerPath string `yaml:"container_path,omitempty" json:"container_path,omitempty"`
}

#ResolvedVolumeMount — one entry of the box-aggregate volume list (CollectBoxVolume): the charly-<box>-<name> volume name + the home-expanded container path. Mirrors deploykit.VolumeMount (a permanent deploykit home, never wire-marshaled there — spec carries the envelope copy).

type Resource

type Resource struct {
	Gpu *GpuSelector `yaml:"gpu,omitempty" json:"gpu,omitempty"`
}

func (*Resource) HasSelector

func (r *Resource) HasSelector() bool

HasSelector reports whether the resource carries a GPU selector.

type ResourceDef

type ResourceDef = Resource

--- top-level entity types ---

type ResourceResolveInput

type ResourceResolveInput struct {
	Resource RawBody `yaml:"resource,omitempty" json:"resource"`
}

type ResourceResolveReply

type ResourceResolveReply struct {
	Resolved *ResolvedResource `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

type RetentionReply

type RetentionReply struct {
	ImageRefs []string `yaml:"image_refs,omitempty" json:"image_refs,omitempty"`

	DanglingIDs []string `yaml:"dangling_ids,omitempty" json:"dangling_ids,omitempty"`

	StagingDirs []string `yaml:"staging_dirs,omitempty" json:"staging_dirs,omitempty"`

	BuildDirs []string `yaml:"build_dirs,omitempty" json:"build_dirs,omitempty"`

	CheckPaths []string `yaml:"check_paths,omitempty" json:"check_paths,omitempty"`

	DeepIDs []string `yaml:"deep_ids,omitempty" json:"deep_ids,omitempty"`

	DeepBytes int64 `yaml:"deep_bytes,omitempty" json:"deep_bytes,omitempty"`

	TagGroups []TagInfo `yaml:"tag_groups,omitempty" json:"tag_groups,omitempty"`

	KeepImages int `yaml:"keep_images,omitempty" json:"keep_images,omitempty"`

	KeepCheckRuns int `yaml:"keep_check_runs,omitempty" json:"keep_check_runs,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

#RetentionReply is the verb:retention reply: the removed (or would-remove, under dry_run) image refs, build-candy dirs, and check-run paths, plus the effective retention counts, for the caller to present.

deep_ids/deep_bytes report the store-wide untagged-image purge (deep): the removed (or would-remove) image IDs and the sum of their reported storage Size in bytes. deep_bytes is an UPPER BOUND, not a prediction of actual freed disk: each image's reported Size counts every layer it references, and many of those layers are SHARED with images that remain (retained tags, or other still-referenced dangling images) — RDD-verified live on a real host, where a --deep purge removing 68 untagged images (3,552 -> 3,484) reported ~92.6 GiB via this sum but only ~4.6 GiB of disk was actually freed (132.6 GB -> 128 GB), because most layer bytes stayed shared with the ~3,400 remaining (largely stale-tagged) images. `charly clean --deep --dry-run` presents deep_bytes as "up to" for exactly this reason; pairing --deep with --invalidate (removing stale TAGS too, so their exclusively-held layers also become unreferenced) gets closer to the reported figure.

tag_groups is the `list` reply payload: every locally stored charly-labeled tag, newest-first per box.

error is a human-facing message on a non-recoverable failure.

type RetentionRequest

type RetentionRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir"`

	DryRun bool `yaml:"dry_run,omitempty" json:"dry_run,omitempty"`

	Images bool `yaml:"images,omitempty" json:"images,omitempty"`

	Check bool `yaml:"check,omitempty" json:"check,omitempty"`

	Deep bool `yaml:"deep,omitempty" json:"deep,omitempty"`

	List bool `yaml:"list,omitempty" json:"list,omitempty"`

	BuildPrune bool `yaml:"build_prune,omitempty" json:"build_prune,omitempty"`

	Keep int `yaml:"keep,omitempty" json:"keep,omitempty"`

	KeepImages int `yaml:"keep_images,omitempty" json:"keep_images,omitempty"`

	KeepCheckRuns int `yaml:"keep_check_runs,omitempty" json:"keep_check_runs,omitempty"`

	Invalidate string `yaml:"invalidate,omitempty" json:"invalidate,omitempty"`
}

#RetentionRequest is the verb:retention request. dir is the project directory (os.Getwd()) — always present, never empty. keep=0 means "use the resolved defaults"; invalidate (non-empty) runs ONLY the targeted image-tag invalidation. deep runs the store-wide untagged/dangling-image purge category (`charly clean --deep`) — unlike images (which only ever touches charly-labeled tags/dangling ids), deep removes EVERY untagged image in local storage, including unlabeled multi-stage build intermediates the images category can never see. list runs the read-only tag inventory (`charly box list tags`) — no dir needed beyond resolving the engine, nothing is removed. build_prune is the narrow post-`charly box build` step: retention-prune image tags + stale .build/_candy staging dirs ONLY (the historic charly/retention.go pruneAfterBuild scope) — deliberately NOT the fuller images category (which also sweeps dangling images + buildah staging), so a build's own post-step behavior is unchanged by this relocation. keep_images/keep_check_runs are the caller's PRE-RESOLVED defaults.keep_images/ keep_check_runs (0 = disabled) — this engine never reads charly.yml itself; a core caller (already running LoadConfig in-process) resolves these directly, a plugin caller resolves them PLUGIN-SIDE via the shared sdk/loaderkit.ResolveRetentionDefaultsViaExecutor (K-wave 2 cone R6 — the former "retention-defaults" HostBuild seam is DELETED). keep (the CLI --keep override) wins over both when > 0.

type ReverseOp

type ReverseOp struct {
	Kind ReverseOpKind `yaml:"kind,omitempty" json:"kind"`

	Format string `yaml:"format,omitempty" json:"format,omitempty"`

	Targets []string `yaml:"targets,omitempty" json:"targets,omitempty"`

	Scope Scope `yaml:"scope,omitempty" json:"scope,omitempty"`

	Extra map[string]string `yaml:"extra,omitempty" json:"extra,omitempty"`

	// uninstall_cmd is the rendered host-venue package-removal command for a
	// ReverseOpPackageRemove op, filled at record time from the format's
	// uninstall_template by fillReverseUninstallCmds.
	UninstallCmd string `yaml:"uninstall_cmd,omitempty" json:"uninstall_cmd,omitempty"`
}

#ReverseOp is a single teardown action. Serialized into the ledger so uninstall can reverse a deploy without re-reading the candy manifest.

type ReverseOpKind

type ReverseOpKind string

ReverseOpKind discriminates the kinds of teardown actions Reverse() produces. Ledger entries serialize these verbatim so a later `charly fleet del` can walk them without re-compiling the plan.

const (
	ReverseOpPackageRemove  ReverseOpKind = "package-remove"
	ReverseOpCargoUninstall ReverseOpKind = "cargo-uninstall"
	ReverseOpNpmUninstallG  ReverseOpKind = "npm-uninstall-g"
	ReverseOpPixiEnvRemove  ReverseOpKind = "pixi-env-remove"
	ReverseOpRmFileSystem   ReverseOpKind = "rm-file-system"
	ReverseOpRmFileUser     ReverseOpKind = "rm-file-user"
	ReverseOpRmDirRecursive ReverseOpKind = "rm-dir-recursive"
	ReverseOpServiceDisable ReverseOpKind = "service-disable"
	ReverseOpServiceRemove  ReverseOpKind = "service-remove"
	ReverseOpRemoveDropin   ReverseOpKind = "remove-dropin"
	ReverseOpRestoreEnabled ReverseOpKind = "restore-enabled"
	ReverseOpRemoveManaged  ReverseOpKind = "remove-managed-block"
	ReverseOpRemoveEnvdFile ReverseOpKind = "remove-envd-file"
	ReverseOpRemoveRepoFile ReverseOpKind = "remove-repo-file"
	ReverseOpCoprDisable    ReverseOpKind = "copr-disable"

	// ReverseOpPluginScript is the GENERIC recordable reverse op an external
	// (out-of-process) deploy/step/builder plugin returns: a shell script + its
	// scope, run verbatim at teardown via the ReverseExecutor (system → sudo,
	// user → no sudo). The script lives in Extra["script"]; Scope picks the
	// privilege. It preserves the record-and-replay invariant — only RECORDED
	// ops are replayed, never recomputed — without any new struct shape.
	ReverseOpPluginScript ReverseOpKind = "plugin-script"
)

type RouteConfig

type RouteConfig struct {
	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	Port string `yaml:"port,omitempty" json:"port,omitempty"`
}

#RouteConfig — a resolved route declaration (host + port-as-string). Mirrors deploykit.RouteConfig. Port is a STRING here (the resolved form), distinct from #CandyRoute.port (authored int).

type RouteYAML

type RouteYAML = CandyRoute

--- candy sub-types ---

type SSHTarget added in v0.2026210.1121

type SSHTarget struct {
	User string
	Host string
	Port int
}

SSHTarget is the parsed form of an ssh-style target string. Examples:

  • "host" → {User: $USER, Host: "host", Port: 22}
  • "user@host" → {User: "user", Host: "host", Port: 22}
  • "user@host:2222" → {User: "user", Host: "host", Port: 2222}
  • "qemu+ssh://user@host/…" → parsed via ParseLibvirtSSHURI (not here)

func ParseSSHTarget added in v0.2026210.1121

func ParseSSHTarget(s string) (SSHTarget, error)

ParseSSHTarget accepts "[user@]host[:port]" and fills defaults:

  • User: $USER
  • Port: 22

func (SSHTarget) String added in v0.2026210.1121

func (t SSHTarget) String() string

String renders the canonical "user@host:port" form.

type SaveDeployStateInput added in v0.2026211.549

type SaveDeployStateInput struct {
	Ports []string `yaml:"ports,omitempty" json:"ports,omitempty"`

	// set_ports gates whether Ports is written to charly.yml at all: `charly config <name>` (no
	// --port flags) and `charly update <name>` must not silently overwrite operator port overrides
	// with image-label defaults.
	SetPorts bool `yaml:"set_ports,omitempty" json:"set_ports,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	CleanEnv bool `yaml:"clean_env,omitempty" json:"clean_env,omitempty"`

	EnvFile string `yaml:"env_file,omitempty" json:"env_file,omitempty"`

	Network string `yaml:"network,omitempty" json:"network,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`

	Volume []DeployVolume `yaml:"volume,omitempty" json:"volume,omitempty"`

	Sidecar map[string]RawBody `yaml:"sidecar,omitempty" json:"sidecar,omitempty"`

	Tunnel *TunnelYAML `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`

	// secret_names lists env var names declared as secret_accepts / secret_requires on the image;
	// saveDeployState defensively strips any matching KEY=VAL entries from both the input Env and
	// the existing persisted entry before writing.
	SecretNames []string `yaml:"secret_names,omitempty" json:"secret_names,omitempty"`

	// Disposable + Lifecycle classification (see /charly-internals:disposable).
	SetDisposable bool `yaml:"set_disposable,omitempty" json:"set_disposable,omitempty"`

	Disposable bool `yaml:"disposable,omitempty" json:"disposable,omitempty"`

	SetLifecycle bool `yaml:"set_lifecycle,omitempty" json:"set_lifecycle,omitempty"`

	Lifecycle string `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"`

	// Box + Target — the schema-required fields per the require-image cutover. Written when non-empty
	// AND when the existing entry doesn't already have a value (don't clobber operator-authored refs).
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Target string `yaml:"target,omitempty" json:"target,omitempty"`

	// resolved_image is the concrete overlay image ref produced by a pod deploy's add_candy: overlay
	// build, persisted by PrepareVenue so config/start deploy EXACTLY that overlay.
	ResolvedImage string `yaml:"resolved_image,omitempty" json:"resolved_image,omitempty"`

	// vm_state + vm_cross_ref — the vm substrate's persisted runtime state + the kind:vm cross-ref,
	// shipped by the externalized vm plugin's PrepareVenue reply as the generic State patch.
	VmState *VmDeployState `yaml:"vm_state,omitempty" json:"vm_state,omitempty"`

	VmCrossRef string `yaml:"vm_cross_ref,omitempty" json:"vm_cross_ref,omitempty"`

	// Resource-arbitration axis: the holder-side Preemptible block + the claimant-side
	// requires_exclusive / requires_shared token lists. Persisted so a deploy/bed MEMBER round-trips
	// its arbiter role through the per-host overlay. Written when non-empty.
	Preemptible *PreemptibleConfig `yaml:"preemptible,omitempty" json:"preemptible,omitempty"`

	RequiresExclusive []string `yaml:"requires_exclusive,omitempty" json:"requires_exclusive,omitempty"`

	RequiresShared []string `yaml:"requires_shared,omitempty" json:"requires_shared,omitempty"`
}

#SaveDeployStateInput holds the deployment parameters SaveDeployState persists to charly.yml (promoted from sdk/deploykit — #55 import-purity, Cone V value-vocabulary; the SaveDeployState FUNCTION stays in deploykit). Every field is a spec/stdlib type. The deploy candies (plugin-deploy-pod, plugin-fleet) pass it DIRECTLY to deploykit.SaveDeployState plugin-side (#55 K4 — no host seam carries it across the wire anymore; the fields are ephemeral in-operation, never persisted as-is — the node-form marshal writes only the resulting entry).

type ScanSeams added in v0.2026213.1726

type ScanSeams struct {
	// CollectRemoteRefs runs the reachability-scoped remote-ref walk over the project's boxes +
	// this local candy set, returning each distinct (repo, git-tag) to fetch. Host closure:
	// CollectRemoteRefsOpts(cfg, FinalizeScannedCandies(localScanned, nil), WithLocalRawRefs(opts, localScanned)).
	CollectRemoteRefs func(localScanned map[string]ScannedCandy) ([]RemoteDownload, error)
	// EnsureRepo resolves a (repoPath, version) to a local cache directory, fetching + auto-migrating
	// on a cache miss (host closure: EnsureRepoDownloaded).
	EnsureRepo func(repoPath, version string) (string, error)
	// ScanRemote scans the wanted bare refs out of a downloaded repo cache dir (host closure:
	// requireCandyScanner().ScanRemoteCandy(cacheDir, repoPath, wantRefs, parseCandyYAML)).
	ScanRemote func(cacheDir, repoPath string, wantRefs map[string]bool) (map[string]ScannedCandy, error)

	// Warn receives the scan's own advisories — candy-version skew and local-shadow notes —
	// instead of them going straight to stderr.
	//
	// They USED to be `fmt.Fprintf(os.Stderr, ...)` calls inside the arbiter, which made them
	// unstructured and therefore uncountable: `charly box validate` could not report how many
	// warnings a run produced, because they never reached its diagnostics. A summary that
	// cannot see them can only omit the number or state a false one.
	//
	// Optional. When nil the advisories still go to stderr exactly as before, so every existing
	// caller keeps its current behaviour and this stays a purely additive seam.
	Warn func(format string, args ...any)
}

scan_seams.go — the host-coupled legs the candy-scan fetch fix-point (loaderkit.ScanCandyFromLocal) reaches. Relocated from sdk/loaderkit/scan_orchestrate.go (#55 C3b-ii) so it can be the parameter type on the spec.ProjectLoader.ScanCandyFromLocal seam method — the interface lives in this dedicated spec module, so its param types must live here too. The caller (charly's scanSeamsFor, candy/plugin-build's scanSeamsLeg) builds these as closures capturing its config/opts + host mechanisms (registry, refs backend); the pure fix-point in loaderkit never inspects a package-main type. loaderkit keeps a `type ScanSeams = spec.ScanSeams` forwarder (mirroring its RemoteDownload alias) so its own signature + candy/plugin-build's call sites stay terse.

type ScanSpec added in v0.2026210.808

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

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

func AnchorScanSpecs added in v0.2026213.1726

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

AnchorScanSpecs returns a copy of `specs` with every relative Path resolved to an absolute path against `srcDir`. Absolute paths are kept verbatim. Empty srcDir leaves specs unchanged so the root-file merge (called with rootDir == workspace) is a no-op. Path-anchoring MECHANISM (filepath) over spec.ScanSpec, relocated from sdk/kit (#55 C3b) so merge.MergeUnified — which itself moved into this spec module — can call it without a spec→sdk import inversion.

func (*ScanSpec) UnmarshalYAML added in v0.2026210.808

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

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

type ScannedCandy

type ScannedCandy struct {
	Model CandyModel
	View  CandyView
	Refs  CandyRefs
}

ScannedCandy gathers one candy's full scan result — the two resolved envelope views plus the rich pre-qualification refs.

SDD classification: same non-wire, same-process pipeline-state rationale as CandyRefs above (one note covers both) — it is the mutable intermediate the whole scan→fetch→qualify→arbitrate pipeline (charly's ScanAllCandy family) carries in place of the pre-move *Candy, until the FINAL step bare-strings the refs (FinalizeCandyRefs) and wraps (Model, View) into a spec.CandyReader via sdk/deploykit.NewSpecCandyModel.

type Scope

type Scope int

Scope classifies what kind of filesystem mutation a step makes. Steps are grouped by scope (and venue) when the host target batches into sudo vs user heredocs — mixing scopes in one batch would need per-command sudo. It is the integer-valued enum the ledger serializes (omitempty omits ScopeSystem=0), so the wire form is unchanged by living in spec.

const (
	// ScopeSystem mutates global host state: /etc, /usr, /var, systemd system
	// units, package DB. Requires sudo on host; emitted as USER root in the
	// Containerfile.
	ScopeSystem Scope = iota

	// ScopeUser mutates the invoking user's home or user-owned paths:
	// $HOME/.pixi, $HOME/.cargo, $HOME/.npm-global, $HOME/.local, systemd
	// user units, etc. No sudo needed on host; emitted as USER ${UID} in the
	// Containerfile.
	ScopeUser

	// ScopeUserProfile writes to the user's shell init surface:
	// ~/.bashrc / ~/.zshenv / fish conf.d + ~/.config/opencharly/env.d/.
	// Separate from ScopeUser because the host target has special handling
	// (managed blocks, shell detection) and the OCI target renders these as
	// ENV directives + path additions rather than file writes.
	ScopeUserProfile
)

func OpStepScope added in v0.2026210.2308

func OpStepScope(userDir string) Scope

func ScopeFromName

func ScopeFromName(name string) Scope

ScopeFromName parses a StepContract's authored scope NAME (the Describe-carried "user"/"user-profile" string; anything else, including empty, defaults to ScopeSystem) into a Scope.

func (Scope) String

func (s Scope) String() string

type ScoreSummary

type ScoreSummary struct {
	Total int `yaml:"total,omitempty" json:"total"`

	Pass int `yaml:"pass,omitempty" json:"pass"`

	Fail int `yaml:"fail,omitempty" json:"fail"`

	Skip int `yaml:"skip,omitempty" json:"skip"`
}

#ScoreSummary — the pass/fail/skip tally block (the former hand-written TestRunSummary). The counts are Go `int` (type=int override — the former hand type; CUE `int` defaults to int64), so every existing ++/compare call site compiles unchanged.

type SecretYAML

type SecretYAML = CandySecret

--- candy sub-types ---

type Security

type Security struct {
	Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"`

	CgroupNS string `yaml:"cgroupns,omitempty" json:"cgroupns,omitempty"`

	CapAdd []string `yaml:"cap_add,omitempty" json:"cap_add,omitempty"`

	Devices []string `yaml:"devices,omitempty" json:"devices,omitempty"`

	SecurityOpt []string `yaml:"security_opt,omitempty" json:"security_opt,omitempty"`

	IpcMode string `yaml:"ipc_mode,omitempty" json:"ipc_mode,omitempty"`

	ShmSize Size `yaml:"shm_size,omitempty" json:"shm_size,omitempty"`

	GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"`

	Mounts []string `yaml:"mount,omitempty" json:"mount,omitempty"`

	MemoryMax Size `yaml:"memory_max,omitempty" json:"memory_max,omitempty"`

	MemoryHigh Size `yaml:"memory_high,omitempty" json:"memory_high,omitempty"`

	MemorySwapMax Size `yaml:"memory_swap_max,omitempty" json:"memory_swap_max,omitempty"`

	Cpus string `yaml:"cpus,omitempty" json:"cpus,omitempty"`
}

--------------------------------------------------------------------------- Container security (SecurityConfig) — shared by box / candy / deploy / sidecar. ---------------------------------------------------------------------------

type SecurityConfig

type SecurityConfig = Security

--- box / deploy sub-types ---

type ServiceCustomStep added in v0.2026210.2308

type ServiceCustomStep struct {
	Name        string // e.g. "charly-ollama-ollama"
	UnitText    string // fully rendered unit content
	UnitPath    string // absolute install path
	TargetScope Scope  // ScopeSystem or ScopeUser
	Enable      bool
	CandyName   string
}

ServiceCustomStep writes and enables a full service definition. The unit text is already rendered by the compiler from the candy's generic `services:` entry via the target init system's service_template.

func (*ServiceCustomStep) Kind added in v0.2026210.2308

func (s *ServiceCustomStep) Kind() StepKind

func (*ServiceCustomStep) RequiresGate added in v0.2026210.2308

func (s *ServiceCustomStep) RequiresGate() Gate

func (*ServiceCustomStep) Reverse added in v0.2026210.2308

func (s *ServiceCustomStep) Reverse() []ReverseOp

func (*ServiceCustomStep) Scope added in v0.2026210.2308

func (s *ServiceCustomStep) Scope() Scope

func (*ServiceCustomStep) Venue added in v0.2026210.2308

func (s *ServiceCustomStep) Venue() Venue

type ServiceEntry

type ServiceEntry = CandyService

--- candy sub-types ---

type ServicePackagedStep added in v0.2026210.2308

type ServicePackagedStep struct {
	Unit          string // e.g. "postgresql.service"
	TargetScope   Scope  // ScopeSystem → /etc/systemd/system/; ScopeUser → ~/.config/systemd/user/
	Enable        bool
	OverridesText string // rendered drop-in (.conf) content; "" if no overrides
	OverridesPath string // computed absolute path for the drop-in file
	CandyName     string // for ledger + drop-in naming
	PriorEnabled  bool   // populated at install time (before enable); used for teardown restore
}

ServicePackagedStep enables a systemd unit that arrived via a distro package (e.g. postgresql.service). Optional drop-in overrides are written alongside the packaged unit without touching it. This step is only emitted when rendering to a systemd-based init system; supervisord targets emit a warning-and-skip.

func (*ServicePackagedStep) Kind added in v0.2026210.2308

func (s *ServicePackagedStep) Kind() StepKind

func (*ServicePackagedStep) RequiresGate added in v0.2026210.2308

func (s *ServicePackagedStep) RequiresGate() Gate

func (*ServicePackagedStep) Reverse added in v0.2026210.2308

func (s *ServicePackagedStep) Reverse() []ReverseOp

func (*ServicePackagedStep) Scope added in v0.2026210.2308

func (s *ServicePackagedStep) Scope() Scope

func (*ServicePackagedStep) Venue added in v0.2026210.2308

func (s *ServicePackagedStep) Venue() Venue

type ServiceRenderContext

type ServiceRenderContext struct {
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	Candy string `yaml:"candy,omitempty" json:"candy,omitempty"`

	Exec string `yaml:"exec,omitempty" json:"exec,omitempty"`

	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	EnvList []EnvKV `yaml:"env_list,omitempty" json:"env_list,omitempty"`

	Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`

	WorkingDirectory string `yaml:"working_directory,omitempty" json:"working_directory,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	After []string `yaml:"after,omitempty" json:"after,omitempty"`

	Before []string `yaml:"before,omitempty" json:"before,omitempty"`

	WantedBy []string `yaml:"wanted_by,omitempty" json:"wanted_by,omitempty"`

	Stdout string `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	StopTimeout string `yaml:"stop_timeout,omitempty" json:"stop_timeout,omitempty"`

	Scope string `yaml:"scope,omitempty" json:"scope,omitempty"`

	PackagedUnit string `yaml:"packaged_unit,omitempty" json:"packaged_unit,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`

	SystemUnitDir string `yaml:"system_unit_dir,omitempty" json:"system_unit_dir,omitempty"`

	UserUnitDir string `yaml:"user_unit_dir,omitempty" json:"user_unit_dir,omitempty"`

	FragmentDir string `yaml:"fragment_dir,omitempty" json:"fragment_dir,omitempty"`

	// Lifecycle directives (supervisord + systemd).
	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

	Events string `yaml:"events,omitempty" json:"events,omitempty"`

	AutoStart *bool `yaml:"auto_start,omitempty" json:"auto_start,omitempty"`

	StartRetries int `yaml:"start_retries,omitempty" json:"start_retries,omitempty"`

	StartSecs int `yaml:"start_secs,omitempty" json:"start_secs,omitempty"`

	StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`

	ExitCodes string `yaml:"exit_codes,omitempty" json:"exit_codes,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`

	// Start hooks (systemd only — supervisord's service_template references
	// neither, exactly as it already ignores wanted_by/before). Carried through
	// from the entry's exec_start_pre/exec_start_post so the template emits one
	// ExecStartPre=/ExecStartPost= line per element, in authored order.
	ExecStartPre []string `yaml:"exec_start_pre,omitempty" json:"exec_start_pre,omitempty"`

	ExecStartPost []string `yaml:"exec_start_post,omitempty" json:"exec_start_post,omitempty"`

	// Portable lifecycle fields, carried verbatim from the entry. Each is honoured
	// by at least two inits; an init whose template does not reference one simply
	// ignores it, exactly as supervisord already ignores wanted_by/before.
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Requires []string `yaml:"requires,omitempty" json:"requires,omitempty"`

	RestartSec string `yaml:"restart_sec,omitempty" json:"restart_sec,omitempty"`

	WatchdogSec string `yaml:"watchdog_sec,omitempty" json:"watchdog_sec,omitempty"`

	// unit_options — init-specific directives, keyed init-name -> directive -> value.
	// A template reads only its OWN key, so this stays ONE field on the shared
	// envelope no matter how many inits exist. See #CandyService.unit_options for
	// why this is a map rather than a field per directive.
	UnitOptions map[string]map[string]any `yaml:"unit_options,omitempty" json:"unit_options,omitempty"`

	// wait_for — the readiness precondition, carried from the entry so each init's
	// template can lower it in its own idiom.
	WaitFor *ServiceWaitFor `yaml:"wait_for,omitempty" json:"wait_for,omitempty"`

	// render_dropin is the host-precomputed drop-in decision (the entry
	// carries Overrides). PackagedUnit != "" selects the packaged branch. The
	// host derives both from the ServiceEntry so the plugin renders from the
	// ctx alone.
	RenderDropin bool `yaml:"render_dropin,omitempty" json:"render_dropin,omitempty"`
}

#ServiceRenderContext is the data an init system's service_template renders against — everything the renderer needs, nothing else reachable. The host computes it per service and hands it to candy/plugin-init's OpResolve.

func BuildServiceRenderContext added in v0.2026213.1726

func BuildServiceRenderContext(entry *ServiceEntry, ctx ServiceRenderContext) ServiceRenderContext

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

type ServiceRenderInput

type ServiceRenderInput struct {
	Init RawBody `yaml:"init,omitempty" json:"init"`

	Ctx ServiceRenderContext `yaml:"ctx,omitempty" json:"ctx"`
}

#ServiceRenderInput is candy/plugin-init's OpResolve input for the service-render leg: the OPAQUE init body (the chosen init system's config) + the host-built render context (all entry-derived fields, home-expanded, with the packaged/drop-in branch decisions precomputed). The plugin renders from the ctx alone.

type ServiceRenderReply

type ServiceRenderReply struct {
	Rendered *RenderedService `yaml:"rendered,omitempty" json:"rendered,omitempty"`
}

#ServiceRenderReply wraps the rendered service.

type ServiceWaitFor added in v0.2026240.1719

type ServiceWaitFor struct {
	// paths — every path must exist before the service starts. Relative paths are
	// resolved by the init against the service's own working directory, exactly as
	// exec: is.
	//
	// Required, and at least one element: a wait_for with no paths waits for
	// nothing while reading like a guard, which is worse than no guard at all.
	Paths []string `yaml:"paths,omitempty" json:"paths"`

	// timeout — how long to wait before failing the start. "30s" or a bare 30,
	// matching stop_timeout's shape. Absent means the init's own default.
	Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`
}

#ServiceWaitFor is a readiness precondition on a service's start.

PATHS only, deliberately. All nine existing loops wait on a filesystem path (a Wayland socket, an IPC socket, a lock file); none polls a TCP port. A ports: form would need a probe tool the base images do not all carry, so it waits for a real second consumer rather than being designed against a hypothetical one.

type Session added in v0.2026242.935

type Session struct {
	// compositor is the binary this session runs, e.g. "Hyprland", "sway", "labwc".
	Compositor string `yaml:"compositor,omitempty" json:"compositor"`

	// syntax is a label for diagnostics and for choosing an escaping strategy.
	Syntax string `yaml:"syntax,omitempty" json:"syntax,omitempty"`

	// config_path_template is where the rendered config lands, as a Go template so a
	// compositor that keys off its own name does not need a second field.
	ConfigPathTemplate string `yaml:"config_path_template,omitempty" json:"config_path_template"`

	// model says whether the constructs concatenate into ONE file (assembly) or each render
	// to its own (file_set). Hyprland and sway are assembly; labwc is a file set.
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	// The per-construct templates. Each renders once per authored entry. A compositor that
	// has no notion of a construct omits the template.
	MonitorTemplate string `yaml:"monitor_template,omitempty" json:"monitor_template,omitempty"`

	BindTemplate string `yaml:"bind_template,omitempty" json:"bind_template,omitempty"`

	InputTemplate string `yaml:"input_template,omitempty" json:"input_template,omitempty"`

	ExecTemplate string `yaml:"exec_template,omitempty" json:"exec_template,omitempty"`

	EnvTemplate string `yaml:"env_template,omitempty" json:"env_template,omitempty"`

	RuleTemplate string `yaml:"rule_template,omitempty" json:"rule_template,omitempty"`

	IncludeTemplate string `yaml:"include_template,omitempty" json:"include_template,omitempty"`

	// extra_file is for the parts of a session that are not one of the constructs above —
	// a bootstrap loader, a portal config.
	ExtraFiles []ThemeRender `yaml:"extra_file,omitempty" json:"extra_file,omitempty"`

	// session_desktop is the /usr/share/wayland-sessions entry a display manager offers.
	// Its `id` is what a displaymanager's `session:` must match, and that cross-check is
	// enforced at load: an autologin naming a session file nothing installed is a black
	// screen at boot with nothing in any log.
	SessionDesktop SessionDesktop `yaml:"session_desktop,omitempty" json:"session_desktop,omitempty"`

	// theme_render lets a session pull colours from a theme entity by name, so a compositor
	// config can be themed without the theme knowing the compositor exists.
	ThemeRender []string `yaml:"theme_render,omitempty" json:"theme_render,omitempty"`
}

#Session is a compositor's RENDERER VOCABULARY — the exact `init:`/`service:` mirror.

The kind says HOW a construct is spelled; the candy's `desktop:` block says WHICH constructs exist. That split is what lets one authored keybinding reach Hyprland's Lua, sway's config syntax and labwc's XML without the author knowing any of them.

A compositor that cannot express a construct simply omits its template, and entries of that kind are dropped with a diagnostic — the same way supervisord ignores `wanted_by`.

type SessionDesktop added in v0.2026242.935

type SessionDesktop struct {
	Id string `yaml:"id,omitempty" json:"id"`

	Name string `yaml:"name,omitempty" json:"name"`

	Exec string `yaml:"exec,omitempty" json:"exec"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`
}

#SessionDesktop is the wayland-sessions entry.

type Shell

type Shell struct {
	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`

	Bash *ShellSpec `yaml:"bash,omitempty" json:"bash,omitempty"`

	Zsh *ShellSpec `yaml:"zsh,omitempty" json:"zsh,omitempty"`

	Fish *ShellSpec `yaml:"fish,omitempty" json:"fish,omitempty"`

	Sh *ShellSpec `yaml:"sh,omitempty" json:"sh,omitempty"`
}

--------------------------------------------------------------------------- Shell-rc config (ShellConfig/ShellSpec) — shared by box + candy. CLOSED: the Go UnmarshalYAML rejects any key outside the 4 intrinsic + 4 shell names. ---------------------------------------------------------------------------

func (*Shell) ByShell

func (s *Shell) ByShell() map[string]*ShellSpec

ByShell returns the per-shell overlay map (nil when none authored).

type ShellEntry

type ShellEntry struct {
	Origin string `yaml:"origin,omitempty" json:"origin"`

	ID string `yaml:"id,omitempty" json:"id,omitempty"`

	Generic *ShellSpec `yaml:"generic,omitempty" json:"generic,omitempty"`

	ByShell map[string]*ShellSpec `yaml:"by_shell,omitempty" json:"by_shell,omitempty"`

	Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
}

#ShellEntry — one origin's shell-init contribution (in ai.opencharly.shell). origin required.

type ShellHookStep added in v0.2026210.2308

type ShellHookStep struct {
	CandyName string
	EnvVars   map[string]string
	PathAdd   []string // already {{.Home}}-substituted to absolute paths
	EnvFile   string   // computed path (~/.config/opencharly/env.d/<candy>.env); populated at install
}

ShellHookStep records the env vars and PATH contributions a candy makes to the user's shell environment. On the OCI target these translate to `ENV K=V` directives in the Containerfile. On the host target they become `~/.config/opencharly/env.d/<candy>.env` plus a managed block in the user's shell init that sources the env.d directory.

func (*ShellHookStep) Kind added in v0.2026210.2308

func (s *ShellHookStep) Kind() StepKind

func (*ShellHookStep) RequiresGate added in v0.2026210.2308

func (s *ShellHookStep) RequiresGate() Gate

func (*ShellHookStep) Reverse added in v0.2026210.2308

func (s *ShellHookStep) Reverse() []ReverseOp

func (*ShellHookStep) Scope added in v0.2026210.2308

func (s *ShellHookStep) Scope() Scope

func (*ShellHookStep) Venue added in v0.2026210.2308

func (s *ShellHookStep) Venue() Venue

type ShellSnippetStep added in v0.2026210.2308

type ShellSnippetStep struct {
	CandyName   string   // candy this snippet belongs to (also Marker source)
	Origin      string   // "<candy>" or "box" or "deploy" (for ledger refcount + LabelShell origin)
	Shell       string   // bash | zsh | fish | sh
	Snippet     string   // rendered body, ${SHELL_NAME}-substituted, ready to write
	PathAppend  []string // already rendered into Snippet by the compiler; tracked here for label round-trip / overlay
	Destination string   // resolved per-target at compile time; absolute path on the target
	Marker      string   // managed-block marker tag (= CandyName) — consumed by the kit walk's managed-block splice (sdk/kit/profile.go)
	UseDropin   bool     // true: write whole file (fish, container drop-in); false: managed-block append into existing rc file
	Priority    int      // load-order hint, 0 = default
}

ShellSnippetStep records a per-(candy, shell) init snippet emitted from a candy's `shell:` block. The compiler emits one step per (candy, shell) pair after applying the selection rule (per-shell ByShell entry wins over generic, with ${SHELL_NAME} substitution).

Per-target rendering:

  • OCITarget: snippet bytes are content-address-staged and COPYed to a system-wide drop-in (/etc/profile.d/charly-<candy>.sh for bash/zsh/sh, /etc/fish/conf.d/charly-<candy>.fish for fish).
  • the external local / vm deploy (kit.WalkPlans on the venue): managed-block append to the user's rc file (~/.bashrc, ~/.zshrc, ~/.profile) keyed by `# opencharly:begin <Marker>` fence; for fish, a per-candy drop-in at ~/.config/fish/conf.d/charly-<candy>.fish (no fence needed, file IS the unit). UseDropin discriminates the two paths.
  • the external kubernetes substrate: does not consume the InstallPlan IR at all (the host generates a Kustomize tree separately — no shell snippet).

func (*ShellSnippetStep) Kind added in v0.2026210.2308

func (s *ShellSnippetStep) Kind() StepKind

func (*ShellSnippetStep) RequiresGate added in v0.2026210.2308

func (s *ShellSnippetStep) RequiresGate() Gate

func (*ShellSnippetStep) Reverse added in v0.2026210.2308

func (s *ShellSnippetStep) Reverse() []ReverseOp

func (*ShellSnippetStep) Scope added in v0.2026210.2308

func (s *ShellSnippetStep) Scope() Scope

Scope: SnippetStep is system-wide for build-mode (Container drop-ins live under /etc/profile.d/ — root-owned), and user-profile for host/vm deploy- mode (managed block in ~/.bashrc etc.). The compiler picks the right Scope when constructing the step based on the destination path; the step records the choice so each emitter doesn't have to re-derive it.

func (*ShellSnippetStep) Venue added in v0.2026210.2308

func (s *ShellSnippetStep) Venue() Venue

type ShellSpec

type ShellSpec struct {
	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	PathAppend []string `yaml:"path_append,omitempty" json:"path_append,omitempty"`

	Path string `yaml:"path,omitempty" json:"path,omitempty"`
}

type Sidecar

type Sidecar struct {
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Image string `yaml:"image,omitempty" json:"image,omitempty"`

	// env / parameter are map[string]string — values MUST be strings (quote
	// YAML bools/numbers). parameter "" is the "deploy must supply" sentinel.
	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	Parameter StrMap `yaml:"parameter,omitempty" json:"parameter,omitempty"`

	Secret []SidecarSecret `yaml:"secret,omitempty" json:"secret,omitempty"`

	Volume []SidecarVolume `yaml:"volume,omitempty" json:"volume,omitempty"`

	Security *Security `yaml:"security,omitempty" json:"security,omitempty"`
}

type SidecarDef

type SidecarDef = Sidecar

--- top-level entity types ---

type SidecarList added in v0.2026218.138

type SidecarList struct {
	Names []string `yaml:"names,omitempty" json:"names,omitempty"`

	Descriptions map[string]string `yaml:"descriptions,omitempty" json:"descriptions,omitempty"`
}

#SidecarList is the sidecar-template-library introspection list — the `charly config --list-sidecars` payload (names + one-line descriptions), returned inside #PodConfigSetupReply.

type SidecarResolveInput

type SidecarResolveInput struct {
	EmbeddedTemplates map[string]RawBody `yaml:"embedded_templates,omitempty" json:"embedded_templates,omitempty"`

	ProjectTemplates map[string]RawBody `yaml:"project_templates,omitempty" json:"project_templates,omitempty"`

	DeployOverrides map[string]RawBody `yaml:"deploy_overrides,omitempty" json:"deploy_overrides,omitempty"`

	CliEnv []string `yaml:"cli_env,omitempty" json:"cli_env,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#SidecarResolveInput is the input to candy/plugin-sidecar's OpResolve leg (the host-side sidecar de-type): the three sidecar-def layers to merge (embedded template base, project-root templates, per-deploy overrides — each a name→Sidecar map the host keeps OPAQUE) plus the CLI -e flags to route and the box/instance for name scoping.

type SidecarResolveReply

type SidecarResolveReply struct {
	Sidecars []ResolvedSidecar `yaml:"sidecars,omitempty" json:"sidecars,omitempty"`

	AppEnv []string `yaml:"app_env,omitempty" json:"app_env,omitempty"`

	PersistOverrides map[string]RawBody `yaml:"persist_overrides,omitempty" json:"persist_overrides,omitempty"`
}

#SidecarResolveReply is candy/plugin-sidecar's OpResolve reply: the resolved, generation-ready sidecars the host feeds to quadlet gen, the CLI env flags NOT routed to any sidecar (app-only), and the per-deploy sidecar overrides with any routed env folded in.

type SidecarSecret

type SidecarSecret struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Env string `yaml:"env,omitempty" json:"env"`

	EnvFrom string `yaml:"env_from,omitempty" json:"env_from,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`
}

type SidecarVolume

type SidecarVolume struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Path string `yaml:"path,omitempty" json:"path"`
}

type Size

type Size = string

--- named-pattern scalars (string) ---

type Skill added in v0.2026218.1950

type Skill struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Family string `yaml:"family,omitempty" json:"family"`

	Owner string `yaml:"owner,omitempty" json:"owner"`

	Description string `yaml:"description,omitempty" json:"description"`

	Content string `yaml:"content,omitempty" json:"content"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"`

	References []SkillReference `yaml:"references,omitempty" json:"references,omitempty"`

	Triggers []string `yaml:"triggers,omitempty" json:"triggers,omitempty"`

	Category string `yaml:"category,omitempty" json:"category,omitempty"`
}

CUE schema for the `skill` KIND — a first-class harness skill entity (the migration of the plugins/ skill corpus into candy configs). A `skill:` node is a sibling top-level entity in a candy's charly.yml (ParseDoc iterates every top-level node, each with its own single kind discriminator), so a candy file stacks its `candy:` node plus the `skill:` nodes it owns.

The FULL skill definition is INLINE: every metadata field plus the markdown content as a block scalar — nothing is fetched from elsewhere. The marketplace generator (candy/plugin-marketplace, command:marketplace) synthesizes plugins/<family>/skills/<name>/SKILL.md + references/*.md from these entities; CollectSkills (sdk/deploykit) projects the composed candies' skills into the `ai.opencharly.skill` OCI label so a built image is self-describing (readable via `charly box labels` / `charly fleet from-box`, no external fetch).

owner is the candy/concept-candy entity name that owns this skill — the build-time association that decides which images carry it (CollectSkills filters by owner ∈ composed candy chain). family is the marketplace family (the plugins/ directory name) the generator groups by.

type: "agent" marks a sub-agent definition (the former plugins/<family>/agents/*.md) — one unified doc kind, same lifecycle; model/tools carry the agent frontmatter fields.

type SkillReference added in v0.2026218.1950

type SkillReference struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Content string `yaml:"content,omitempty" json:"content"`
}

type StandaloneLoad

type StandaloneLoad struct {
	Shape string `yaml:"shape,omitempty" json:"shape"`

	Deploy *Deploy `yaml:"deploy,omitempty" json:"deploy,omitempty"`

	Template RawBody `yaml:"template,omitempty" json:"template,omitempty"`

	Box *Box `yaml:"box,omitempty" json:"box,omitempty"`

	Candy *Candy `yaml:"candy,omitempty" json:"candy,omitempty"`
}

#StandaloneLoad carries a structural kind's host-pre-decoded canonical node. Exactly one of Deploy / Template / Box / Candy is set, matching Shape.

type Status

type Status int

status_result.go — the check-engine's pass/fail/skip verdict enum (FLOOR-SLIM Unit 4, moved from sdk/kit as part of the CheckResult wire-envelope split). HAND-WRITTEN — a distinct named int type with the iota consts + String() method, NOT emitted by `task cue:gen` (gengotypes has no construct for an iota-based enum + a Stringer method; CUE owns the wire VALUE SET via #CheckResult.status's plain `int` shape, referenced here via @go(Status,type=Status) — this file supplies the named Go type that reference points at). Mirrors the #SubstrateKind split in status_types.go (a string-backed enum suppressed via @go(-) with its Go type hand-written here-style) — Status is int-backed, so there is no separate disjunction def to suppress; the CUE field is a plain `int`.

const (
	StatusPass Status = iota
	StatusFail
	StatusSkip
)

func (Status) String

func (s Status) String() string

String renders the lowercase verdict word. Reporters uppercase it for display (strings.ToUpper → "PASS"/"FAIL"/"SKIP").

type StatusNestedNode

type StatusNestedNode struct {
	Key string `yaml:"key,omitempty" json:"key"`

	Path string `yaml:"path,omitempty" json:"path"`

	Kind SubstrateKind `yaml:"kind,omitempty" json:"kind"`

	HasChildren bool `yaml:"has_children,omitempty" json:"has_children"`

	MatchKeys []string `yaml:"match_keys,omitempty" json:"match_keys,omitempty"`

	LiveStatus string `yaml:"live_status,omitempty" json:"live_status,omitempty"`

	Children []*StatusNestedNode `yaml:"children,omitempty" json:"children,omitempty"`
}

#StatusNestedNode — one pre-resolved node of the DECLARED nested tree, now built PLUGIN-SIDE (candy/plugin-status/nested_tree.go, K5) directly from the resolved-project envelope + deploykit.LoadFleetConfig/MergeDeployConfigs/ClassifyTarget/ResolveDeployChain — all sdk-portable, so this is no longer a host-computed wire payload; it stays a CUE def only because #StatusNestedNode is self-recursive and the plugin still needs the generated Go type. key is the declared child key (the Image cell). match_keys index the flat rows; for a ROOT node key itself is the flat match. RECURSIVE self-reference mirrors the deploy tree nesting.

type StatusSubstrateReply

type StatusSubstrateReply struct {
	Rows []DeploymentStatus `yaml:"rows,omitempty" json:"rows,omitempty"`

	Single DeploymentStatus `yaml:"single,omitempty" json:"single,omitempty"`
}

#StatusSubstrateReply — the host-collection result: the flat rows (all substrates, already probed) and — on the single path — the one detail row. The declared nested roots are no longer carried here (K5: resolved plugin-side, see #StatusNestedNode) — the candy computes its own roots and applies the PURE overlay(rows, roots) then renders.

type StatusSubstrateRequest

type StatusSubstrateRequest struct {
	Single bool `yaml:"single,omitempty" json:"single,omitempty"`

	IncludeAll bool `yaml:"include_all,omitempty" json:"include_all,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#StatusSubstrateRequest — the host-collection request the command:status plugin sends over HostBuild("status-substrate"). single=true selects the pod-scoped detail path (box+instance); otherwise the full multi-substrate fan-out (include_all mirrors --all). The declared nested tree (formerly resolved here via `nested`) is now resolved PLUGIN-SIDE (candy/plugin-status's own buildStatusRootsTree, K5) directly off the resolved-project envelope + deploykit.LoadFleetConfig — no host round-trip, so this request carries no nested flag anymore.

type Step

type Step struct {
	Run        string `yaml:"run,omitempty"         json:"run,omitempty"`
	Check      string `yaml:"check,omitempty"       json:"check,omitempty"`
	AgentRun   string `yaml:"agent-run,omitempty"   json:"agent-run,omitempty"`
	AgentCheck string `yaml:"agent-check,omitempty" json:"agent-check,omitempty"`
	Include    string `yaml:"include,omitempty"     json:"include,omitempty"`

	Op `yaml:",inline" json:",inline"`
}

Step is a single plan step. Exactly one of Run/Check/AgentRun/AgentCheck/Include is non-empty (validated by StepKind()). The embedded Op (generated) is inline-promoted.

func (*Step) IsAgent

func (s *Step) IsAgent() bool

IsAgent reports whether the step is agent-graded (agent-run / agent-check).

func (*Step) IsInclude

func (s *Step) IsInclude() bool

IsInclude reports whether the step is an include: composition directive.

func (*Step) KeywordText

func (s *Step) KeywordText() string

KeywordText returns the populated keyword's prose regardless of which discriminator holds it.

func (*Step) Mutates

func (s *Step) Mutates() bool

Mutates reports whether the step changes system state (run / agent-run). `charly check live` (verify-only mode) skips mutating steps.

func (*Step) StepKind

func (s *Step) StepKind() (StepKeyword, error)

StepKind returns the step's intent keyword and an error if zero or multiple keyword discriminators are set.

type StepBatch

type StepBatch struct {
	Scope Scope
	Venue Venue
	Steps []InstallStep
}

StepBatch is a contiguous run of steps sharing the same (Scope, Venue). Emitted together: one sudo heredoc, one user heredoc, or one podman run per batch.

type StepContract

type StepContract struct {
	Scope Scope
	Venue Venue
	Gate  Gate
	// Emits is the F-STEP-EMIT flag: the step produces a build-context Containerfile FRAGMENT
	// (the serving plugin answers Invoke(OpEmit) → EmitReply.Fragment). The pod-overlay
	// deploykit.OCITarget consults it via the open external-step arm — Emits=true → bake the
	// fragment; Emits=false → skip (a deploy-only external step, like apk on an image build).
	// Advisory for the DEPLOY leg (executeExternalStep ignores it); load-bearing for the BUILD
	// leg (ociEmitStep).
	Emits bool
}

StepContract is a class:step plugin's DECLARED install-step contract (F3), decoded from its Describe capability (pb.StepContract / sdk.StepContract). compileActOp reads it (via StepContractCarrier) to build an externalStep carrying the plugin-declared Scope/Venue/Gate — the contract the host applies via the open default arm with NO compiled-in case.

type StepContractCarrier

type StepContractCarrier interface {
	DeclaredStepContract() (StepContract, bool)
}

StepContractCarrier is implemented by a provider (grpcProvider out-of-proc, inprocProvider compiled-in) that carries a class:step capability's declared StepContract. A false return means the provider declares no step contract (every non-step capability).

type StepEmitRequest

type StepEmitRequest struct {
	Word string `yaml:"word,omitempty" json:"word"`

	Payload RawBody `yaml:"payload,omitempty" json:"payload,omitempty"`

	Distros []string `yaml:"distros,omitempty" json:"distros,omitempty"`
}

#StepEmitRequest is the F-STEP-EMIT HostBuild envelope for a HOST-COUPLED external step kind's build-context fragment.

type StepKeyword

type StepKeyword string

StepKeyword is the intent discriminator on a Step.

const (
	KwRun        StepKeyword = "run"
	KwCheck      StepKeyword = "check"
	KwAgentRun   StepKeyword = "agent-run"
	KwAgentCheck StepKeyword = "agent-check"
	KwInclude    StepKeyword = "include"
)

type StepKind

type StepKind string

StepKind names the concrete type behind an InstallStep. Used for ledger serialization and target-specific dispatch (e.g. HostDeploy knows to invoke the pixi builder differently than cargo).

const (
	StepKindSystemPackages  StepKind = "SystemPackages"
	StepKindBuilder         StepKind = "Builder"
	StepKindOp              StepKind = "Op"
	StepKindFile            StepKind = "File"
	StepKindServicePackaged StepKind = "ServicePackaged"
	StepKindServiceCustom   StepKind = "ServiceCustom"
	StepKindShellHook       StepKind = "ShellHook"
	StepKindShellSnippet    StepKind = "ShellSnippet"
	StepKindRepoChange      StepKind = "RepoChange"
	StepKindApkInstall      StepKind = "ApkInstall"
	StepKindLocalPkgInstall StepKind = "LocalPkgInstall"
	StepKindReboot          StepKind = "Reboot"
	StepKindExternalPlugin  StepKind = "ExternalPlugin"
	StepKindExtract         StepKind = "Extract"
)

type StepPass added in v0.2026213.1726

type StepPass struct {
	Stdout string `yaml:"stdout,omitempty" json:"stdout,omitempty"`

	Stderr string `yaml:"stderr,omitempty" json:"stderr,omitempty"`

	ExitCode int `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`
}

#StepPass is the verbatim stdout/stderr/exit-code of a host-delegated guest sub-invocation (the nested pod-in-VM check-live delegation, runVm's guestNestedCheckCmd path). The plugin writes Stdout/Stderr and returns ExitCode unchanged, so a guest-run check reports byte-identically to a direct one. CUE-sourced (SDD): plain carrier, no json:"-".

type StepRecord added in v0.2026215.1748

type StepRecord struct {
	Kind StepKind `yaml:"kind,omitempty" json:"kind"`

	Scope Scope `yaml:"scope,omitempty" json:"scope,omitempty"`

	Venue Venue `yaml:"venue,omitempty" json:"venue,omitempty"`

	Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`

	CompletedAt string `yaml:"completed_at,omitempty" json:"completed_at"`

	Extra map[string]string `yaml:"extra,omitempty" json:"extra,omitempty"`
}

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

type StepResult added in v0.2026213.1726

type StepResult struct {
	Keyword string `yaml:"keyword,omitempty" json:"keyword"`

	Text string `yaml:"text,omitempty" json:"text"`

	Origin string `yaml:"origin,omitempty" json:"origin,omitempty"`

	StepID string `yaml:"step_id,omitempty" json:"step_id"`

	Result CheckResult `yaml:"result,omitempty" json:"result"`
}

#StepResult is one plan step's outcome — the step's identity (keyword/text/origin/step_id) plus the CheckResult of running it; the result reporters consume a []StepResult. CUE-sourced (SDD): a plain carrier — every field is a string or the CUE-sourced #CheckResult by value, with NO json:"-" and NO disjunction, so gengotypes generates it faithfully (byte-identical JSON to the former hand-written spec.StepResult). The P12 gengotypes exception (cited at sdk/kit/checkrun_seam.go + schema/seam.cue) covers ONLY kit.CheckResult's engine-internal `DeadlineExceeded bool json:"-"` — a field that exists in memory but is excluded from marshaling — which lives on the kit-internal ENGINE wrapper (struct { spec.CheckResult; DeadlineExceeded bool json:"-" }), NOT on this wire type: StepResult.Result is spec.CheckResult (the CUE-sourced base, no json:"-"), so the wire form carries only spec.CheckResult fields and the kit wrapper adds DeadlineExceeded in memory only, dropped at the StepResult boundary.

type StepScore

type StepScore struct {
	ID string `yaml:"id,omitempty" json:"id"`

	Origin string `yaml:"origin,omitempty" json:"origin,omitempty"`

	Text string `yaml:"text,omitempty" json:"text,omitempty"`

	Tag []string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Keyword string `yaml:"keyword,omitempty" json:"keyword,omitempty"`

	Verb string `yaml:"verb,omitempty" json:"verb,omitempty"`

	Status string `yaml:"status,omitempty" json:"status"`

	SkippedReason string `yaml:"skipped_reason,omitempty" json:"skipped_reason,omitempty"`
}

#StepScore — the scorer's verdict for one check:/agent-check: step, keyed by step id.

type StrMap

type StrMap = map[string]string

StrMap is the Go shape of #StrMap (`{[string]: #StrVal}`). charly decodes every env / var / parameter / oci_label map into map[string]string — yaml.v3 coerces an unquoted scalar (`PORT: 8080`) to its literal text, so the map stays string→string. Source: the StrMap usage across charly/*.go.

type StrVal

type StrVal any /* CUE disjunction: (bool|string|number) */

An env entry: KEY=VALUE (value may be empty / contain =). A YAML scalar Go decodes into a string (map[string]string / string field): yaml.v3 coerces an unquoted int/bool/float to its literal text, so an idiomatic `PORT: 8080` is a valid string value. #StrMap is the matching map.

type StructuralKindCarrier

type StructuralKindCarrier interface {
	IsStructuralKind() bool
}

StructuralKindCarrier is implemented by a provider (grpcProvider out-of-proc, inprocProvider compiled-in) that carries a class:kind capability's STRUCTURAL flag (F5). true → the kind's OpLoad returns a Deploy member tree the host folds into uf.Fleet; false (or not implemented) → the flat F4 path (opaque body → uf.PluginKinds).

type StructuralKindLoadEnv

type StructuralKindLoadEnv struct {
	Members map[string]*Deploy `yaml:"members,omitempty" json:"members,omitempty"`

	// standalone is the host-pre-decoded CANONICAL node channel (candy/plugin-substrate,
	// candy/plugin-candy).
	Standalone *StandaloneLoad `yaml:"standalone,omitempty" json:"standalone,omitempty"`
}

#StructuralKindLoadEnv is the OpLoad invocation context (op.Env) the host threads to a STRUCTURAL class:kind plugin (F5 authored-member input-threading).

type SubstrateKind

type SubstrateKind string

SubstrateKind identifies which deployment substrate a DeploymentStatus row came from — the discriminator that lets `charly status` present pod, VM, kubernetes, local, and android deployments side-by-side from one unified table. The command:status plugin (fan-out + render) and every substrate plugin's status-collect Op share this type; #DeploymentStatus.kind references it via @go(Kind,type=SubstrateKind).

HAND-WRITTEN — a distinct named string type with the substrate consts, NOT emitted by `task cue:gen` (gengotypes references it from the generated DeploymentStatus.Kind field but does not define it; a CUE string enum would degrade to a plain string and lose the typed consts the collectors switch on).

const (
	SubstratePod        SubstrateKind = "pod"
	SubstrateVM         SubstrateKind = "vm"
	SubstrateKubernetes SubstrateKind = "kubernetes"
	SubstrateLocal      SubstrateKind = "local"
	SubstrateAndroid    SubstrateKind = "android"
)

type SubstrateStatusReply

type SubstrateStatusReply struct {
	Rows []DeploymentStatus `yaml:"rows,omitempty" json:"rows,omitempty"`

	Single DeploymentStatus `yaml:"single,omitempty" json:"single,omitempty"`
}

#SubstrateStatusReply — the per-substrate COLLECTOR result: the LIVE rows the substrate plugin produced (pod: snapshot-derived + live mounts + tool probes; local: install-ledger rows). The host applies the deploy enrichment (tunnel + image-label fallback) to the pod rows after they cross this seam; the single field carries the one pod detail row.

type SubstrateStatusRequest

type SubstrateStatusRequest struct {
	IncludeAll bool `yaml:"include_all,omitempty" json:"include_all,omitempty"`

	RunMode string `yaml:"run_mode,omitempty" json:"run_mode"`

	QuadletDir string `yaml:"quadlet_dir,omitempty" json:"quadlet_dir"`

	EngineBin string `yaml:"engine_bin,omitempty" json:"engine_bin"`

	Single bool `yaml:"single,omitempty" json:"single,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
}

#SubstrateStatusRequest — the per-substrate COLLECTOR request the host sends to the substrate plugin's OpStatusCollect (P14a: the cleanly-movable collectors — pod live + local + the probes — relocated into candy/plugin-substrate, served on the kind provider's Invoke by word pod/vm/kubernetes/local/android). The host passes the scalar inputs a sdk-only candy cannot derive: the engine binary name (engine_bin), the run mode, the quadlet dir (pod's quadlet-description enrichment + enabled-but-not-running append), include_all (--all), and — on the single path — box+instance. NO deploy-cone (FleetConfig/UnifiedFile) crosses this seam: the deploy enrichment stays host-side until K5, applied to the live rows this reply returns. vm/kubernetes/android are deferred to K5 (their collectors are deploy-cone-coupled); the plugin returns no rows for those words until then.

type SubstrateTemplateResolveRequest

type SubstrateTemplateResolveRequest struct {
	Local *LocalResolveInput `yaml:"local,omitempty" json:"local,omitempty"`

	Android *AndroidResolveInput `yaml:"android,omitempty" json:"android,omitempty"`

	Pod *PodResolveInput `yaml:"pod,omitempty" json:"pod,omitempty"`

	Kubernetes *KubernetesResolveInput `yaml:"kubernetes,omitempty" json:"kubernetes,omitempty"`

	Vm *VmResolveInput `yaml:"vm,omitempty" json:"vm,omitempty"`
}

#SubstrateTemplateResolveRequest is the discriminated OpResolve request for the substrate-template de-type: exactly one of Local / Android / Pod is set.

type Subvolume added in v0.2026242.753

type Subvolume struct {
	// name is the subvolume as created, e.g. "@" or "@home".
	Name string `yaml:"name,omitempty" json:"name"`

	// mount_point is the guest-absolute path it is mounted at, e.g. "/" or "/home".
	MountPoint string `yaml:"mount_point,omitempty" json:"mount_point"`

	// mount_options are extra comma-joined mount options, e.g. "compress=zstd,noatime".
	// `subvol=<name>` is always prepended by the emitter and must not be repeated here.
	MountOptions string `yaml:"mount_options,omitempty" json:"mount_options,omitempty"`
}

#Subvolume is one btrfs subvolume in a #DiskLayout.

type SystemInfo added in v0.2026241.931

type SystemInfo struct {
	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`

	// distro_id is the host distro identifier (e.g. "fedora", "arch", "debian").
	DistroID string `yaml:"distro_id,omitempty" json:"distro_id,omitempty"`

	// distro_version is the host distro version string (e.g. "43", "rolling").
	DistroVersion string `yaml:"distro_version,omitempty" json:"distro_version,omitempty"`

	// kernel is the running kernel release (uname -r).
	Kernel string `yaml:"kernel,omitempty" json:"kernel,omitempty"`

	// arch is the host architecture (uname -m).
	Arch string `yaml:"arch,omitempty" json:"arch,omitempty"`

	// gpu is the primary GPU description (vendor + model), when detectable.
	GPU string `yaml:"gpu,omitempty" json:"gpu,omitempty"`

	// virtualization is the detected virtualization backend (kvm, qemu, none).
	Virtualization string `yaml:"virtualization,omitempty" json:"virtualization,omitempty"`

	// podman is the podman version string, when present.
	Podman string `yaml:"podman,omitempty" json:"podman,omitempty"`

	// updated_at is when this snapshot was last refreshed (RFC3339).
	UpdatedAt string `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
}

#SystemInfo is the top-level `system:` block — the host identity snapshot.

type SystemPackagesStep added in v0.2026210.2308

type SystemPackagesStep struct {
	Format   string     // "rpm" | "deb" | "pac"
	Phase    Phase      // PhasePrepare | PhaseInstall | PhaseCleanup
	Packages []string   // package names (for Reverse)
	Repos    []RepoSpec // repository entries from the candy manifest (drives PhasePrepare)
	Options  []string   // format-specific install flags
	Copr     []string   // RPM-only: COPR repos to enable/disable
	Modules  []string   // RPM-only: DNF modules to enable
	Exclude  []string   // RPM-only: packages to exclude
	Keys     []string   // PAC-only: GPG keys to trust

	// CacheMounts and RawInstallContext are passed to template rendering.
	// These are populated by the compiler and consumed by the OCI target;
	// the host target ignores CacheMounts (no BuildKit outside containers).
	CacheMount        []CacheMountSpec
	RawInstallContext map[string]any
}

SystemPackagesStep installs packages via a distro package manager. One step typically expands to (PhasePrepare + PhaseInstall + PhaseCleanup) stages; the compiler emits one SystemPackagesStep per phase so the host target can gate PhasePrepare on --allow-repo-changes.

func (*SystemPackagesStep) Kind added in v0.2026210.2308

func (s *SystemPackagesStep) Kind() StepKind

func (*SystemPackagesStep) RequiresGate added in v0.2026210.2308

func (s *SystemPackagesStep) RequiresGate() Gate

func (*SystemPackagesStep) Reverse added in v0.2026210.2308

func (s *SystemPackagesStep) Reverse() []ReverseOp

func (*SystemPackagesStep) Scope added in v0.2026210.2308

func (s *SystemPackagesStep) Scope() Scope

func (*SystemPackagesStep) Venue added in v0.2026210.2308

func (s *SystemPackagesStep) Venue() Venue

type TagInfo

type TagInfo struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Ref string `yaml:"ref,omitempty" json:"ref"`

	Version string `yaml:"version,omitempty" json:"version"`

	InUse bool `yaml:"in_use,omitempty" json:"in_use"`
}

#TagInfo is one locally stored image tag, as presented by `charly box list tags` (verb:retention list=true): version is "-" when the image carries no parseable ai.opencharly.version label.

type TagPkgConfig

type TagPkgConfig struct {
	Package []string `yaml:"package,omitempty" json:"package,omitempty"`

	Raw map[string]any `yaml:"raw,omitempty" json:"raw,omitempty"`
}

#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:. Mirrors deploykit.TagPkgConfig.

type TargetHop

type TargetHop struct {
	Transport string `yaml:"transport,omitempty" json:"transport"`

	Address string `yaml:"address,omitempty" json:"address,omitempty"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	Port int64 `yaml:"port,omitempty" json:"port,omitempty"`

	IdentityFile string `yaml:"identity_file,omitempty" json:"identity_file,omitempty"`

	Command []string `yaml:"command,omitempty" json:"command,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	// Transport-native argv options expressed as deterministic key/value
	// data. SSH renders these as repeated `-o key=value` arguments; no shell
	// interpolation is involved.
	Options StrMap `yaml:"options,omitempty" json:"options,omitempty"`
}

type TargetSpec

type TargetSpec struct {
	// Ordered outer-to-inner route after placement. An empty route is the
	// selected process. deployment/instance first place that process in any
	// Charly deployment; the remaining route composes identically there.
	Hops []TargetHop `yaml:"hops,omitempty" json:"hops,omitempty"`

	Deployment string `yaml:"deployment,omitempty" json:"deployment,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
}

type TerminalExit

type TerminalExit struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id"`

	ExitCode int64 `yaml:"exit_code,omitempty" json:"exit_code"`

	Reason string `yaml:"reason,omitempty" json:"reason,omitempty"`

	Time string `yaml:"time,omitempty" json:"time"`
}

type TerminalFrame

type TerminalFrame struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id"`

	Sequence int64 `yaml:"sequence,omitempty" json:"sequence"`

	Kind string `yaml:"kind,omitempty" json:"kind"`

	Stream string `yaml:"stream,omitempty" json:"stream,omitempty"`

	Data []byte `yaml:"data,omitempty" json:"data,omitempty"`

	Snapshot string `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`

	Status string `yaml:"status,omitempty" json:"status,omitempty"`

	ExitCode *int `yaml:"exit_code,omitempty" json:"exit_code,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`
}

type TerminalInput

type TerminalInput struct {
	Kind string `yaml:"kind,omitempty" json:"kind"`

	Text string `yaml:"text,omitempty" json:"text,omitempty"`

	Key string `yaml:"key,omitempty" json:"key,omitempty"`

	Cols int64 `yaml:"cols,omitempty" json:"cols,omitempty"`

	Rows int64 `yaml:"rows,omitempty" json:"rows,omitempty"`

	Signal string `yaml:"signal,omitempty" json:"signal,omitempty"`
}

type TerminalKey

type TerminalKey struct {
	Key string `yaml:"key,omitempty" json:"key"`
}

type TerminalProfile

type TerminalProfile struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Entrypoint []string `yaml:"entrypoint,omitempty" json:"entrypoint"`

	WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`

	Env StrMap `yaml:"env,omitempty" json:"env,omitempty"`

	Cols int64 `yaml:"cols,omitempty" json:"cols"`

	Rows int64 `yaml:"rows,omitempty" json:"rows"`

	Readiness map[string]any `yaml:"readiness,omitempty" json:"readiness,omitempty"`

	SemanticAdapter string `yaml:"semantic_adapter,omitempty" json:"semantic_adapter,omitempty"`

	Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"`

	Signals []string `yaml:"signals,omitempty" json:"signals,omitempty"`

	Persistence string `yaml:"persistence,omitempty" json:"persistence,omitempty"`

	Transcript string `yaml:"transcript,omitempty" json:"transcript,omitempty"`
}

type TerminalResize

type TerminalResize struct {
	Cols int64 `yaml:"cols,omitempty" json:"cols"`

	Rows int64 `yaml:"rows,omitempty" json:"rows"`
}

type TerminalSnapshot

type TerminalSnapshot struct {
	RunID UUIDv7 `yaml:"run_id,omitempty" json:"run_id"`

	Sequence int64 `yaml:"sequence,omitempty" json:"sequence"`

	Cols int64 `yaml:"cols,omitempty" json:"cols"`

	Rows int64 `yaml:"rows,omitempty" json:"rows"`

	Screen string `yaml:"screen,omitempty" json:"screen"`

	CursorCol int64 `yaml:"cursor_col,omitempty" json:"cursor_col,omitempty"`

	CursorRow int64 `yaml:"cursor_row,omitempty" json:"cursor_row,omitempty"`
}

type Theme added in v0.2026242.935

type Theme struct {
	// name is the theme's identity, e.g. "tokyo-night". Lowercase and hyphenated, matching
	// what a theme-switcher command accepts.
	Name string `yaml:"name,omitempty" json:"name"`

	// variant tells applications which system palette to pair with. Closed: these are the
	// only two values the freedesktop colour-scheme preference has.
	Variant string `yaml:"variant,omitempty" json:"variant,omitempty"`

	// token is the colour palette. accent, foreground and background are REQUIRED because a
	// theme that defines none of them cannot render anything legible; everything else is
	// optional so a minimal theme stays small.
	Token ThemeTokens `yaml:"token,omitempty" json:"token"`

	// font, cursor_theme and icon_theme are the non-colour half of a theme. They are named
	// separately from token because they are resource NAMES, not colours, and a template
	// interpolating them into a font stack must not be able to reach the colour namespace.
	Font string `yaml:"font,omitempty" json:"font,omitempty"`

	CursorTheme string `yaml:"cursor_theme,omitempty" json:"cursor_theme,omitempty"`

	IconTheme string `yaml:"icon_theme,omitempty" json:"icon_theme,omitempty"`

	// background lists wallpaper files, candy-relative. First entry is the default. These
	// lower to `copy:` rather than `write:` — a JPEG base64'd through a plan is neither
	// readable nor small.
	Background []string `yaml:"background,omitempty" json:"background,omitempty"`

	// render is where the theme becomes files. Each entry names an application and the file
	// to write for it, as a Go template over {{.Token.*}}, {{.Font}}, {{.CursorTheme}},
	// {{.IconTheme}} and {{.Variant}}.
	Render []ThemeRender `yaml:"render,omitempty" json:"render,omitempty"`
}

#Theme is a colour scheme plus the per-application files it renders into.

The token vocabulary is CLOSED. A template referencing {{.Token.accnt}} is a typo that would render an empty colour — a subtly wrong desktop rather than a failure — so OpValidate rejects any template naming a token this entity does not define. That check is the whole reason this is a kind.

Nothing here names a compositor, so the same entity serves Hyprland, sway, labwc and KDE.

type ThemeRender added in v0.2026242.935

type ThemeRender struct {
	// app is a label for diagnostics — "foot", "btop", "hyprland". It is not looked up.
	App string `yaml:"app,omitempty" json:"app"`

	// path is where the file lands, ${HOME}-relative or absolute.
	Path string `yaml:"path,omitempty" json:"path"`

	Content string `yaml:"content,omitempty" json:"content"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	// scope selects whose file it is; user means run_as the image user.
	Scope string `yaml:"scope,omitempty" json:"scope,omitempty"`

	// distro restricts this file to matching distro TAGS, for the cases where one desktop
	// spells a path differently across distros.
	//
	// [...string], not [...#DistroID], and deliberately: these are the same free-form TAGS
	// a candy's `distro:` list carries (box.cue does the same), not the closed guest-distro
	// id a VM source is validated against. #DistroID is also `@go(-)` — it generates no Go
	// type — so a list of it would emit a reference to a type that does not exist.
	Distro []string `yaml:"distro,omitempty" json:"distro,omitempty"`
}

#ThemeRender is one file a theme writes.

type ThemeTokens added in v0.2026242.935

type ThemeTokens struct {
	Accent HexColor `yaml:"accent,omitempty" json:"accent"`

	Foreground HexColor `yaml:"foreground,omitempty" json:"foreground"`

	Background HexColor `yaml:"background,omitempty" json:"background"`

	Cursor HexColor `yaml:"cursor,omitempty" json:"cursor,omitempty"`

	SelectionForeground HexColor `yaml:"selection_foreground,omitempty" json:"selection_foreground,omitempty"`

	SelectionBackground HexColor `yaml:"selection_background,omitempty" json:"selection_background,omitempty"`

	// The 16 ANSI slots a terminal palette needs. Named individually rather than as a list
	// so a template says {{.Token.color4}} and a missing one is a load error, not an index
	// panic at render time.
	Color0 HexColor `yaml:"color0,omitempty" json:"color0,omitempty"`

	Color1 HexColor `yaml:"color1,omitempty" json:"color1,omitempty"`

	Color2 HexColor `yaml:"color2,omitempty" json:"color2,omitempty"`

	Color3 HexColor `yaml:"color3,omitempty" json:"color3,omitempty"`

	Color4 HexColor `yaml:"color4,omitempty" json:"color4,omitempty"`

	Color5 HexColor `yaml:"color5,omitempty" json:"color5,omitempty"`

	Color6 HexColor `yaml:"color6,omitempty" json:"color6,omitempty"`

	Color7 HexColor `yaml:"color7,omitempty" json:"color7,omitempty"`

	Color8 HexColor `yaml:"color8,omitempty" json:"color8,omitempty"`

	Color9 HexColor `yaml:"color9,omitempty" json:"color9,omitempty"`

	Color10 HexColor `yaml:"color10,omitempty" json:"color10,omitempty"`

	Color11 HexColor `yaml:"color11,omitempty" json:"color11,omitempty"`

	Color12 HexColor `yaml:"color12,omitempty" json:"color12,omitempty"`

	Color13 HexColor `yaml:"color13,omitempty" json:"color13,omitempty"`

	Color14 HexColor `yaml:"color14,omitempty" json:"color14,omitempty"`

	Color15 HexColor `yaml:"color15,omitempty" json:"color15,omitempty"`
}

#ThemeTokens is the closed colour vocabulary. Every value is a #HexColor: a token that accepted arbitrary strings would let "blue" through and render a broken config.

type Threaded

type Threaded struct {
	Kinds            map[string]bool   // recognizedKind
	DeploySubstrates map[string]bool   // recognizedDeploySubstrate
	StructuralKinds  map[string]bool   // externalKindMayNestMembers
	Primaries        map[string]string // pluginPrimaryFor: verb word → scalar-sugar primary field
	// DeployTraits is the substrate word → DECLARED #DeployTraits map (K1-LOADER RELOCATION): the
	// descent stamp reads THIS registry-derived DATA snapshot instead of querying the provider
	// registry live, so the venue-hop descent stamp (loaderkit.StampFleetDescents) is registry-free
	// and can run plugin-side. The host fills it (deployTraitsFor per recognized kind/substrate word)
	// exactly as the former live stampFleetDescents did; a word absent from the map resolves to the
	// external-in-place default, matching deployTraitsFor's nil-for-unrecognized-word semantics.
	DeployTraits map[string]*DeployTraits
	// ExternalDeploySubstrates is the EXACT set of words for which the host's registry-live
	// isExternalDeploySubstrate returns true (K1-LOADER RELOCATION): the host fills it by EVALUATING
	// its own isExternalDeploySubstrate over every recognized substrate word, so a plugin-side
	// validator (loaderkit.ValidateCheckBeds) checks membership here instead of RECONSTRUCTING the
	// host's resourceKindSet ∧ externalizedDeploySubstrates ∧ recognizedDeploySubstrate decision —
	// the byte-exact predicate is threaded, never approximated. A word absent from the set is
	// NOT an external deploy substrate (isExternalDeploySubstrate would return false for it).
	ExternalDeploySubstrates map[string]bool
}

Threaded is the host-computed, registry-derived DATA the per-document parse consults instead of querying the provider registry (boundary law clause D): which words are recognized kinds / deploy substrates, which external kinds may nest sub-entity members, and each plugin verb's scalar-sugar primary field. The host snapshots it before the parse; the parse never touches the registry.

type ToolStatus

type ToolStatus struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Status string `yaml:"status,omitempty" json:"status"`

	Port int `yaml:"port,omitempty" json:"port,omitempty"`

	Detail string `yaml:"detail,omitempty" json:"detail,omitempty"`
}

#ToolStatus — one live tool-probe result row. status "-" means the tool is not configured in this deployment; it is filtered before render.

type TunnelConfig

type TunnelConfig struct {
	// provider — "tailscale" or "cloudflare".
	Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`

	// tunnel_name — cloudflare: tunnel name.
	TunnelName string `yaml:"tunnel_name,omitempty" json:"tunnel_name,omitempty"`

	// hostname — cloudflare: default hostname (from the image dns field).
	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`

	// box_name — for PID file naming / cloudflare tunnel-name default.
	BoxName string `yaml:"box_name,omitempty" json:"box_name,omitempty"`

	// ports — all tunneled ports with their access scope.
	Ports []TunnelPort `yaml:"ports,omitempty" json:"ports,omitempty"`
}

#TunnelConfig — the resolved, ready-to-execute tunnel configuration.

func TunnelConfigFromMetadata added in v0.2026213.1726

func TunnelConfigFromMetadata(meta *BoxMetadata) *TunnelConfig

TunnelConfigFromMetadata creates a TunnelConfig from image-label metadata. It is the SINGLE tunnel-resolution entry point — the former TunnelYAML-based resolution function (with its unused CandyReader/[]string DI params) was deleted (R5): its only production caller, the deploy-overlay `charly box inspect` tunnel display, resolves the same shape by constructing a BoxMetadata from the overlay's Tunnel + published-port set.

type TunnelPort

type TunnelPort struct {
	// port — the tailscale HTTPS listen port (must be a valid serve/funnel port).
	Port int `yaml:"port,omitempty" json:"port,omitempty"`

	// backend_port — the localhost backend port (0 means same as port).
	BackendPort int `yaml:"backend_port,omitempty" json:"backend_port,omitempty"`

	// protocol — backend scheme: http | https | https+insecure | tcp |
	// tls-terminated-tcp | ssh | rdp | smb (udp is skipped, never tunneled).
	Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"`

	// public — true = internet-accessible (funnel), false = private (serve).
	Public bool `yaml:"public,omitempty" json:"public,omitempty"`

	// hostname — cloudflare: per-port hostname (from the map form).
	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
}

#TunnelPort — a single port to tunnel with its protocol and access scope.

func (TunnelPort) Backend

func (tp TunnelPort) Backend() int

Backend returns the localhost backend port for a tunneled port, defaulting to Port when BackendPort is unset (0). Pure structural method on the generated spec.TunnelPort wire type (schema/tunnel.cue) — the ONE home (R3) for the backend-port default the pod quadlet emitter (sdk/deploykit) consumes. Lives here beside the type it acts on (the charly_methods.go convention): touches ONLY the type's own fields + the stdlib.

type TunnelYAML

type TunnelYAML struct {
	Provider string    `yaml:"provider" json:"provider"`
	Tunnel   string    `yaml:"tunnel,omitempty" json:"tunnel,omitempty"`
	Public   PortScope `yaml:"public,omitempty" json:"public"`
	Private  PortScope `yaml:"private,omitempty" json:"private"`
}

TunnelYAML supports both bare string and expanded form (the `tunnel:` field).

type UUIDv7

type UUIDv7 string

type UnifiedDeployTarget added in v0.2026218.138

type UnifiedDeployTarget interface {
	// Name is the deployment's identifier from charly.yml (e.g.
	// "arch-vm", "sway-pod"). Unique within a charly.yml.
	Name() string

	// Kind returns one of "host" | "vm" | "pod" | "kubernetes".
	// Drives ledger keying ("<kind>:<name>") and command dispatch.
	Kind() string

	// Executor returns the DeployExecutor this target will use for
	// shell operations. For host → ShellExecutor; for vm →
	// SSHExecutor; for pod → a podman-exec wrapper; for kubernetes → a nop
	// executor that errors on invocation (kubernetes operates via
	// kubectl/Kustomize, not shell ops).
	//
	// Exposing the executor on the interface lets parent targets in
	// a nested tree compose a NestedExecutor over the child.
	Executor() DeployExecutor

	// Add applies the given plans to the target. Equivalent to
	// `charly fleet add <name>`. Idempotent: re-applying the same plan
	// is safe. dctx carries the dispatch-merged node + loaded configs;
	// the adapter constructs its live embedded target from it (never
	// re-reading the node from disk — see DeployContext).
	Add(ctx context.Context, dctx *DeployContext, plans []*InstallPlan, opts EmitOpts) error

	// Del reverses every candy currently recorded for this target
	// and removes the deploy record. Equivalent to `charly fleet del
	// <name>`. Only recorded ReverseOps are replayed — never an
	// ad-hoc computation from the candy manifest.
	Del(ctx context.Context, opts DeployTargetDelOpts) error

	// Update re-applies the plan diff between the currently-recorded
	// candy set and the plan set derived from fresh charly.yml.
	// Equivalent to `charly fleet update <name>` (new command; today's
	// `charly update` is image-focused and will be separate).
	Update(ctx context.Context, plans []*InstallPlan, opts UpdateOpts) error
}

UnifiedDeployTarget is the unified contract all five deploy methods (local, vm, pod, kubernetes, android) implement uniformly. Each method corresponds to an `charly fleet …` subcommand, so the dispatcher in ResolveTarget (charly/unified_targets.go) can route purely on target.Kind() without per-cmd switches. Every method dispatches through the ONE generic DeployTargetDispatchRequest envelope, discriminated by an `op` field.

type UnifiedFile added in v0.2026210.808

type UnifiedFile struct {
	Version string `yaml:"version,omitempty" json:"version,omitempty"`
	// Repo is this project's canonical repo identity (e.g. "github.com/opencharly/charly").
	// Optional; only meaningful on the ROOT file.
	Repo string `yaml:"repo,omitempty" json:"repo,omitempty"`
	// Import is the SINGLE composition statement. A list whose items are either a bare string
	// (flat import into THIS root namespace) or a single-key map `alias: ref` (a namespaced
	// child import). See ImportList (load_directives.go).
	Import   ImportList     `yaml:"import,omitempty" json:"import,omitempty"`
	Discover DiscoverConfig `yaml:"discover,omitempty" json:"discover,omitempty"`
	// Defaults carries the `defaults:` block (BoxConfig-shaped inheritance defaults).
	Defaults BoxConfig `yaml:"defaults,omitempty" json:"defaults,omitempty"`
	// Box is the generic kind-keyed IMAGE map (P6): name → opaque marshaled BoxConfig.
	Box BoxMap `yaml:"box,omitempty" json:"box,omitempty"`
	// Candy is the generic kind-keyed LAYER map: name → opaque marshaled InlineCandy.
	Candy map[string]json.RawMessage `yaml:"candy,omitempty" json:"candy,omitempty"`
	// Fleet is the flat name-keyed deploy map (the canonical `deploy:` surface).
	Fleet    map[string]FleetNode `yaml:"deploy,omitempty" json:"deploy,omitempty"`
	Provides *ProvidesConfig      `yaml:"provides,omitempty" json:"provides,omitempty"`

	// Cache is the per-host LOCAL cache status (git metadata cache). Lives in the
	// per-host charly.yml (~/.config/charly/charly.yml) — the single home for local
	// system state — NOT in ad-hoc JSON files under ~/.cache/charly/repos. Shape:
	// schema/cache.cue #CacheConfig.
	Cache *CacheConfig `yaml:"cache,omitempty" json:"cache,omitempty"`
	// Ledger is the per-host INSTALL LEDGER (deploy/candy/step records). The
	// unified home for what is installed on this host, replacing the per-deploy
	// JSON files under ~/.config/opencharly/installed/. Shape: schema/ledger.cue
	// #LedgerConfig.
	Ledger *LedgerConfig `yaml:"ledger,omitempty" json:"ledger,omitempty"`
	// System is the per-host LOCAL SYSTEM INFO (hostname, distro, kernel, arch,
	// GPU, …). Shape: schema/system.cue #SystemInfo.
	System *SystemInfo `yaml:"system,omitempty" json:"system,omitempty"`

	// PluginKinds holds entities of KINDS contributed by plugins (a kind the core has no typed
	// map for). NAME-KEYED: kind word → entity NAME → canonical body (opaque JSON). Built-in
	// kinds decode into their typed maps above. Host-internal — never serialized.
	PluginKinds map[string]map[string]json.RawMessage `yaml:"-" json:"-"`

	// Namespaces holds child namespaces mounted by namespaced `import:` entries (alias →
	// fully-resolved isolated UnifiedFile). NOT authored directly — populated by the
	// materialize pass. Entries are referenced qualified, e.g. `base: cachyos.cachyos`.
	Namespaces map[string]*UnifiedFile `yaml:"-"`

	// RootDir is this UnifiedFile's OWN base directory — the dir its root document's SrcDir
	// names. Set once per materialize, for both the top-level project and each mounted
	// namespace. Empty for a project-less / synthetic UnifiedFile.
	RootDir string `yaml:"-"`
}

UnifiedFile is the full schema of a single unified-format YAML document. Every field is optional — a file with only `distro:` is valid (typical for the embedded build vocabulary); a file with only `deploy:` is valid; etc.

func (*UnifiedFile) Android added in v0.2026210.808

func (uf *UnifiedFile) Android() map[string]json.RawMessage

func (*UnifiedFile) BoxConfig added in v0.2026210.808

func (uf *UnifiedFile) BoxConfig(name string) (BoxConfig, bool)

BoxConfig decodes the authored image config for name; ok=false when absent.

func (*UnifiedFile) BoxNames added in v0.2026210.808

func (uf *UnifiedFile) BoxNames() []string

BoxNames returns the image names, sorted.

func (*UnifiedFile) CheckBeds added in v0.2026210.808

func (uf *UnifiedFile) CheckBeds() map[string]FleetNode

CheckBeds returns the disposable R10 beds keyed by name. In the unified node-form model a bed IS a `disposable: true` fleet, so the bed set is derived directly from the disposable fleets in the Fleet map. Members are instruments (brought up alongside a driver), never standalone beds. Single enumeration source for `charly check run <bed>` (and the /verify-beds fan-out).

func (*UnifiedFile) HasBox added in v0.2026210.808

func (uf *UnifiedFile) HasBox(name string) bool

HasBox reports whether an image named name is present.

func (*UnifiedFile) Kubernetes added in v0.2026223.1018

func (uf *UnifiedFile) Kubernetes() map[string]json.RawMessage

func (*UnifiedFile) Local added in v0.2026210.808

func (uf *UnifiedFile) Local() map[string]json.RawMessage

func (*UnifiedFile) Pod added in v0.2026210.808

func (uf *UnifiedFile) Pod() map[string]json.RawMessage

func (*UnifiedFile) ProjectConfig added in v0.2026210.808

func (uf *UnifiedFile) ProjectConfig() *Config

ProjectConfig returns the *Config equivalent of uf (the box config view).

func (*UnifiedFile) ProjectTemplates added in v0.2026210.808

func (uf *UnifiedFile) ProjectTemplates() *ProjectTemplates

ProjectTemplates decodes the uf.Local/Kubernetes/Pod/VM/Android raw template maps (map[string]json.RawMessage) into the resolved kind-template maps validate/check-include/status read. Returns nil when no template kind is present. Recurses into uf.Namespaces, mirroring FillBoxPlans's prefix-accumulation pattern, so a namespace-qualified template ref (`local: <ns>.<tmpl>`, `kind:kubernetes` entity `<ns>.<name>`, …) is visible in the envelope too. Purely ADDITIVE (qualified keys never collide with a bare name, since a bare name can never contain "."), so every existing root-scoped consumer is unaffected.

This is UnifiedFile's own projection (relocated from loaderkit.ProjectTemplates as a METHOD — the free function's name collided with the ProjectTemplates TYPE; it is the sibling of ProjectConfig above).

func (*UnifiedFile) SetBox added in v0.2026210.808

func (uf *UnifiedFile) SetBox(name string, b BoxConfig)

SetBox stores an authored image config under name (marshaling it opaque).

func (*UnifiedFile) SetCandy added in v0.2026210.808

func (uf *UnifiedFile) SetCandy(name string, il *InlineCandy)

SetCandy stores a layer under name (marshaling it opaque).

func (*UnifiedFile) VM added in v0.2026210.808

func (uf *UnifiedFile) VM() map[string]json.RawMessage

VM/Pod/Kubernetes/Local/Android are DERIVED accessors over uf.PluginKinds[disc] — the 5 standalone-substrate-TEMPLATE kinds fold into PluginKinds generically (no per-kind-word switch). Each returns the opaque name→body map for its kind (nil when none configured); the kernel never decodes the bodies itself — consuming PLUGINS decode a body into the concrete kind they need.

type UpdateOpts added in v0.2026218.138

type UpdateOpts struct {
	DryRun           bool
	AssumeYes        bool
	RebuildImage     bool
	AllowRepoChanges bool
	AllowRootTasks   bool
	WithServices     bool
}

UpdateOpts parameterizes `charly fleet update`. On the wire Update marshals the SAME spec.LifecycleOpts shape Add does (R3 — one wire shape for the shared apply body); RebuildImage is deliberately NOT threaded there — it belongs to DeployTargetRebuildOpts.

type UserInfo

type UserInfo struct {
	Found bool `yaml:"found,omitempty" json:"found"`

	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	UID int `yaml:"uid,omitempty" json:"uid,omitempty"`

	GID int `yaml:"gid,omitempty" json:"gid,omitempty"`

	Home string `yaml:"home,omitempty" json:"home,omitempty"`
}

#UserInfo — one /etc/passwd entry (name:uid:gid:...:home). Empty reply (found=false) when no user with the requested uid exists in the image.

type VFIOGpu

type VFIOGpu struct {
	Addr string `yaml:"addr,omitempty" json:"addr"`

	GroupMembers []VFIOPCIDevice `yaml:"group_members,omitempty" json:"group_members"`

	VendorID string `yaml:"vendor_id,omitempty" json:"vendor_id"`

	DeviceID string `yaml:"device_id,omitempty" json:"device_id"`

	Class string `yaml:"class,omitempty" json:"class"`

	ClassLabel string `yaml:"class_label,omitempty" json:"class_label"`

	Driver string `yaml:"driver,omitempty" json:"driver"`

	IOMMUGroup int `yaml:"iommu_group,omitempty" json:"iommu_group"`
}

#VFIOGpu is a display-class device plus every other function sharing its IOMMU group. Passthrough must move the whole group together, so the renderer emits one <hostdev> per GroupMember.

FLATTENED (never a Go anonymous-embedded field — CUE unification has no concept of Go embedding; it always structurally merges fields into one flat struct). The former hand type embedded VFIOPCIDevice anonymously; Go's encoding/json ALREADY promotes an anonymous embedded struct's fields to the parent JSON object on the wire (Go's own embedding-promotion rule), so this flattened shape is BYTE-IDENTICAL JSON to the former embedded type — the only consumer-visible change is Go SOURCE: a composite literal naming the embedded field (`VFIOGpu{VFIOPCIDevice: d}`) must spread d's fields instead (`VFIOGpu{Addr: d.Addr, ...}`). Fixed at every call site in the same cutover (candy/plugin-gpu, charly test fixtures).

func NewVFIOGpu

func NewVFIOGpu(d VFIOPCIDevice) VFIOGpu

gpu_methods.go — a pure Go CONSTRUCTOR for the CUE-generated, FLATTENED VFIOGpu (sdk/schema/gpu.cue -> spec/cue_types_gen.go). VFIOGpu used to embed VFIOPCIDevice anonymously (Go's own JSON-promotion rule already made the wire shape identical either way); CUE unification has no concept of Go embedding, so the SDD conversion flattened VFIOGpu's fields directly. NewVFIOGpu restores the one-line "build a VFIOGpu from a VFIOPCIDevice" ergonomic the former embedded-field composite literal (`VFIOGpu{VFIOPCIDevice: d}`) provided, so every construction site (charly, candy/plugin-gpu) names ONE spread point instead of repeating the 7-field copy (R3) — set GroupMembers on the returned value afterward, exactly as the former embedded-literal call sites already did.

func SelectGPUByVendor

func SelectGPUByVendor(rep VFIOReport, vendor string) (VFIOGpu, bool)

SelectGPUByVendor returns the first passthrough-capable GPU whose PCI vendor matches (case/prefix-insensitive). ok=false when no GPU matches.

type VFIOPCIDevice

type VFIOPCIDevice struct {
	Addr string `yaml:"addr,omitempty" json:"addr"`

	VendorID string `yaml:"vendor_id,omitempty" json:"vendor_id"`

	DeviceID string `yaml:"device_id,omitempty" json:"device_id"`

	Class string `yaml:"class,omitempty" json:"class"`

	ClassLabel string `yaml:"class_label,omitempty" json:"class_label"`

	Driver string `yaml:"driver,omitempty" json:"driver"`

	IOMMUGroup int `yaml:"iommu_group,omitempty" json:"iommu_group"`
}

#VFIOPCIDevice is a single PCI function discovered under sysfs.

type VFIOReport

type VFIOReport struct {
	IOMMUEnabled bool `yaml:"iommu_enabled,omitempty" json:"iommu_enabled"`

	IOMMUKind string `yaml:"iommu_kind,omitempty" json:"iommu_kind"`

	GPUs []VFIOGpu `yaml:"gpus,omitempty" json:"gpus"`
}

#VFIOReport summarizes host readiness for VFIO GPU passthrough.

type ValidateProjectReply

type ValidateProjectReply struct {
	Project *ResolvedProject `yaml:"project,omitempty" json:"project,omitempty"`

	Diagnostics Diagnostics `yaml:"diagnostics,omitempty" json:"diagnostics,omitempty"`
}

#ValidateProjectReply — the error-TOLERANT #ResolvedProject projection (partial; only boxes and candies that resolved) + the per-box resolve-failure diagnostics. The plugin merges these with its own pure-rule, raw-config, CUE-conformance and resolution-graph findings for the verdict.

type ValidateProjectRequest

type ValidateProjectRequest struct {
	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	IncludeDisabled bool `yaml:"include_disabled,omitempty" json:"include_disabled,omitempty"`
}

#ValidateProjectRequest — which project dir to validate (empty = the host's cwd) + whether to include enabled:false boxes. Mirrors #ResolvedProjectRequest (the sibling resolved-project seam).

type ValidateWordSetsReply added in v0.2026218.138

type ValidateWordSetsReply struct {
	ProviderCapabilities []string `yaml:"provider_capabilities,omitempty" json:"provider_capabilities,omitempty"`

	ActCapableVerbs []string `yaml:"act_capable_verbs,omitempty" json:"act_capable_verbs,omitempty"`
}

#ValidateWordSetsReply — the two registry-derived D-data word sets the validate rules consume as membership sets. provider_capabilities is every compiled-in provider as "<class>:<word>" (the TARGET set a `source: builtin` plugin candy's declared providers must be a member of); act_capable_verbs is the subset of the request's plugin_words whose act form has a build/deploy install path (the check act-form rule).

type ValidateWordSetsRequest added in v0.2026218.138

type ValidateWordSetsRequest struct {
	PluginWords []string `yaml:"plugin_words,omitempty" json:"plugin_words,omitempty"`

	ExternalProviders []string `yaml:"external_providers,omitempty" json:"external_providers,omitempty"`
}

#ValidateWordSetsRequest — the plugin-supplied inventory the host answers registry-D over.

plugin_words are the DISTINCT `plugin:` verb words the plugin enumerated from its OWN envelope (every candy model's plan plus every box plan); the host reports which of them ACT in build/deploy. external_providers are the `plugin.providers:` capability strings ("<class>:<word>") of every candy whose `plugin.source:` is a real out-of-tree ref — the host must LEARN those declarations before it can answer, because a declared-but-not-yet-connected external verb/step is act-capable by declaration alone (there is no provider to resolve until the build connects it).

type ValidatingKindCarrier

type ValidatingKindCarrier interface {
	IsValidatingKind() bool
}

ValidatingKindCarrier is implemented by a provider (grpcProvider out-of-proc, inprocProvider compiled-in) that carries a class:kind capability's VALIDATES flag (F7/C8). true → the host dispatches OpValidate to the kind at load (a deep plugin-owned check returning Diagnostics, beyond the static CUE input-def gate); false (or not implemented) → only the static gate runs.

type ValidationError added in v0.2026210.808

type ValidationError struct {
	Errors []string
}

ValidationError accumulates project/candy validation error messages (the loader validation accumulator, distinct from the richer Diagnostics — a plain []string collector). Relocated from loaderkit (resolve_opts.go) with UnifiedFile so the type-only loader-result consumers share ONE definition.

func (*ValidationError) Add added in v0.2026210.808

func (e *ValidationError) Add(format string, args ...any)

Add adds an error to the collection.

func (*ValidationError) Error added in v0.2026210.808

func (e *ValidationError) Error() string

func (*ValidationError) HasErrors added in v0.2026210.808

func (e *ValidationError) HasErrors() bool

HasErrors returns true if there are any errors.

type Venue

type Venue int
const (
	// VenueHostNative runs commands directly on the host (or as a RUN in
	// the Containerfile for the container target). The step's shell
	// commands execute natively; no isolation container.
	VenueHostNative Venue = iota

	// VenueContainerBuilder runs the step inside the existing multi-stage
	// builder image (fedora-builder / arch-builder / ...). On the
	// host target this means `podman run --user <host-uid> -v <paths>
	// <builder> bash -c "..."`. On the OCI target this is a FROM + RUN
	// pair that becomes its own build stage, with copy_artifacts pulling
	// outputs into the final image.
	VenueContainerBuilder

	// VenueSkip records the step with a reason but doesn't execute. Used
	// for container-runtime-only fields (ports:, volumes:, tunnel:, …)
	// when compiling for the host target, and for aur: on non-Arch hosts.
	VenueSkip
)

func (Venue) String

func (v Venue) String() string

type VenueDescriptor

type VenueDescriptor struct {
	Kind string `yaml:"kind,omitempty" json:"kind"`

	User string `yaml:"user,omitempty" json:"user,omitempty"`

	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	Port int `yaml:"port,omitempty" json:"port,omitempty"`

	Args []string `yaml:"args,omitempty" json:"args,omitempty"`

	ConnectTimeout int `yaml:"connect_timeout,omitempty" json:"connect_timeout,omitempty"`

	// engine/container_name are set ONLY for kind "container": the container engine
	// ("podman"/"docker", selecting JumpPodmanExec vs JumpDockerExec) and the target container
	// name ContainerChain jumps into.
	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"`
}

#VenueDescriptor is the SELF-CONTAINED, serializable description of a deploy venue's executor that a substrate LIFECYCLE plugin's OpPrepareVenue / OpTeardownExecutor returns (F6). K1-unblock W3 Unit B added the "container" kind (engine/container_name) so a plugin-constructed deploykit.ContainerChain venue — a single-hop *NestedExecutor{Parent:ShellExecutor{}, Jump:{Kind:JumpPodmanExec|JumpDockerExec}}, the MOST COMMON check-runner venue — round-trips through kit.DescriptorFromExecutor/VenueFromDescriptor exactly like "shell"/"ssh" already do. This does NOT generalize to arbitrary N-hop composition (a genuinely multi-hop NestedExecutor still degrades to the zero descriptor, unchanged) — it closes the one enumerable, well-known single-hop shape ContainerChain always produces.

type VerbSpec

type VerbSpec struct {
	Contexts   []ExecContext
	DefaultDo  DoMode
	Reversible bool
}

VerbSpec is the per-verb metadata in VerbCatalog. Contexts[0] is the canonical default context.

func (VerbSpec) HasContext

func (s VerbSpec) HasContext(ctx ExecContext) bool

HasContext reports whether the verb is legal in ctx.

type VerifyChecksRequest added in v0.2026213.1726

type VerifyChecksRequest struct {
	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`

	VerifyOnly bool `yaml:"verify_only,omitempty" json:"verify_only,omitempty"`

	Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`

	Venue VenueDescriptor `yaml:"venue,omitempty" json:"venue,omitempty"`
}

#VerifyChecksRequest is the command:check OpVerifyChecks envelope (#55 CHECK-ENGINE cone, Unit 2): the host threads a live venue executor — flattened to #VenueDescriptor, since a live executor cannot cross the wire, and re-materialized PLUGIN-SIDE via kit.VenueFromDescriptor (the SAME mechanism candy/plugin-fleet's resolveRootExecutor uses) — over the in-proc reverse channel and asks the COMPILED-IN command:check to DRIVE a deploy-scope check pass PLUGIN-SIDE. This sheds charly core's checkrun.go + planrun_adapter.go sdk/kit imports (the in-proc kit.Runner construction moved plugin-side).

ONE drive shape now: plan → the `target: local` --verify path (candy/plugin-fleet's verify_local.go, #55 W3 B3 — a PEER plugin now, not core): a PLUGIN-ASSEMBLED plan (kind:local template, resolved via node_resolve.go's lookupLocalTemplate — no LoadUnified — + deploy node; the per-host overlay merge happens on THIS side) driven via kit.RunPlan (verify-only/context/ keyword gating). The plugin rebuilds the runtime env (USER/HOME/IMAGE/INSTANCE) + ${HOST:} host-vars + the cross-deployment TargetResolver from {dir, box, instance} — plugin-check ALREADY does this for check-live (verb_resolver.go / members.go), so those never cross the wire.

The former SECOND drive shape (ops/only_ids — the deploy-lifecycle Test path, charly core's unified_targets.go runUnifiedTargetChecks feeding raw deploy-scope Op checks via kit.Runner.Run, no plan gating) is GONE (#55 W3 B3 remainder): its own sole production caller, pluginDeployTarget.Test (UnifiedDeployTarget's Test method), had ZERO real callers anywhere in the tree — `charly check live` reaches candy/plugin-check directly (live_gather.go) and never touches this interface method; the ONE caller was a unit test. Test()/runUnifiedTargetChecks/ TestOpts (charly), verifyChecksRunOps/filterOpsByID (candy/plugin-check), and the dead "test" op in #DeployTargetDispatchRequest's enum are all deleted together. The box-mode context-skip regression coverage (TestLiveVerb_SkipsUnderBoxMode, charly/checkrun_charly_verbs_test.go) moved onto the surviving plan shape — RunOne (sdk/kit/planrun.go) is the SAME per-step primitive both kit.Runner.Run and kit.RunPlan dispatched through, so the context-vs-mode gate (opInContext) is identically exercised either way; no coverage was lost.

The reply is []#StepResult (CUE-sourced in this file) — CONSUMED, not modified. All plain fields (plan/venue are spec envelope types; StepResult.Result is #CheckResult by value) → gengotypes-faithful, no @go(-).

type Vm

type Vm struct {
	Source VmSource `yaml:"source,omitempty" json:"source"`

	DiskSize string `yaml:"disk_size,omitempty" json:"disk_size,omitempty"`

	Ram string `yaml:"ram,omitempty" json:"ram,omitempty"`

	Cpus int `yaml:"cpu,omitempty" json:"cpu,omitempty"`

	Machine string `yaml:"machine,omitempty" json:"machine,omitempty"`

	// firmware is REQUIRED-with-default (not optional): an if-guard can only
	// reference a field that always resolves to a concrete value, and an
	// OPTIONAL field — even one carrying a default — errors with "cannot
	// reference optional field" when absent. Required-with-default materializes
	// "bios" on omission (matching the Go empty→bios behavior) AND stays
	// referenceable by the uefi-secure cross-rule below.
	Firmware string `yaml:"firmware,omitempty" json:"firmware"`

	Backend string `yaml:"backend,omitempty" json:"backend"`

	Autostart bool `yaml:"autostart,omitempty" json:"autostart"`

	Network *VmNetwork `yaml:"network,omitempty" json:"network,omitempty"`

	SSH *VmSsh `yaml:"ssh,omitempty" json:"ssh,omitempty"`

	CloudInit *VmCloudInit `yaml:"cloud_init,omitempty" json:"cloud_init,omitempty"`

	Libvirt *LibvirtDomain `yaml:"libvirt,omitempty" json:"libvirt,omitempty"`

	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`

	Snapshots []VmSnapshot `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`
}

type VmBoxMetadata added in v0.2026246.554

type VmBoxMetadata struct {
	// distro — the guest distro id. Validated against the #DistroID vocabulary (the
	// closed id list the VM sources' `distro:` are checked against) at read time; a
	// plain string here so a future id needs no schema bump (the #DistroID def is
	// @go(-): it generates no Go type, so the field must be a string on the wire).
	Distro string `yaml:"distro,omitempty" json:"distro,omitempty"`

	// arch — the guest architecture (x86_64 | aarch64).
	Arch string `yaml:"arch,omitempty" json:"arch,omitempty"`

	// base_user — the adopted account the box is built around (the cloud_image source
	// adoption pattern: cloud-init's default user the image was prepared with).
	BaseUser string `yaml:"base_user,omitempty" json:"base_user,omitempty"`

	// ssh_user — the ssh login user the deploy host connects as.
	SSHUser string `yaml:"ssh_user,omitempty" json:"ssh_user,omitempty"`

	// firmware — the guest firmware mode the box boots under.
	Firmware string `yaml:"firmware,omitempty" json:"firmware,omitempty"`

	// init — the guest init system (systemd | openrc).
	Init string `yaml:"init,omitempty" json:"init,omitempty"`

	// charly_install — how the charly guest agent gets in: auto | scp | skip.
	CharlyInstall string `yaml:"charly_install,omitempty" json:"charly_install,omitempty"`

	// version — the box's CalVer (v0.<YYYYDDD>.<HHMM>).
	Version string `yaml:"version,omitempty" json:"version,omitempty"`

	// source — provenance: which #VmSource kind produced the disk artifact and from
	// what. The generic BoxRef resolver (cutover task 4) routes every arm onto its
	// existing primitive from this record.
	Source VmBoxSource `yaml:"source,omitempty" json:"source,omitempty"`

	// description — human-facing description of the box.
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	// plan — the baked check plan (the SAME #Step the #LabeledDescription plan
	// carries): the acceptance surface `charly check live` walks after a from-box
	// vm: deploy — the VM analog of the pod box's baked plan.
	Plan []Step `yaml:"plan,omitempty" json:"plan,omitempty"`
}

CUE schema for the VM-box metadata contract — the VM-specific half of the OCI-label metadata a VM box image carries (cutover plan §2.1: "a VM box is an OCI image whose labels carry the VM metadata contract and whose layers carry the disk artifact"). A VM box image carries the STANDARD #BoxMetadata labels (the shared ai.opencharly.* hub) AND, layered ON TOP of them, #VmBoxMetadata — the VM-specific facts pods never carry (guest distro/arch, adopted account, firmware, init system, install strategy, disk provenance). Runtime-relevant the same way #BoxMetadata is: the box emitter bakes it at build time (sdk/deploykit EmitVmBox) and the source-less VM deploy (VmCapabilitiesFromLabels) reads it back. CUE-sourced here so the whole VM-box resolution reads ONE spec-owned type. Package-less; concatenated into spec.

R8 (the byte-freeze gate): #VmBoxMetadata IS whole-struct-marshaled into ONE label (ai.opencharly.vm.box, spec.LabelVmBox) — like #CapabilityService → ai.opencharly.service. Because the label is NEW it has no pre-existing hand-struct anchor to freeze, so its json tags ARE the wire contract (required `!` → no omitempty, optional `?` → omitempty) — the deploykit reader round-trips this struct byte-for-byte through the label.

type VmBoxSource added in v0.2026246.554

type VmBoxSource struct {
	Kind string `yaml:"kind,omitempty" json:"kind"`

	// clone: the source entity the box's disk was cloned from.
	FromVm string `yaml:"from_vm,omitempty" json:"from_vm,omitempty"`

	// clone: the base snapshot id the clone overlay was created from.
	FromSnapshot string `yaml:"from_snapshot,omitempty" json:"from_snapshot,omitempty"`

	// bootc: the candy image ref the disk was installed from.
	Box string `yaml:"box,omitempty" json:"box,omitempty"`

	// cloud_image | iso: the artifact url the disk was fetched from.
	URL string `yaml:"url,omitempty" json:"url,omitempty"`
}

#VmBoxSource — provenance of a VM box's disk artifact: the source kind that produced it plus the kind-specific origin reference. The kind space mirrors #VmSource's arms (cloud_image | bootc | clone | bootstrap | iso) so every arm a VM was built from leaves a resolvable provenance record; one arm's fields are populated per kind.

type VmBuildReply

type VmBuildReply struct {
	SourceKind string `yaml:"source_kind,omitempty" json:"source_kind"`

	VmJSON RawBody `yaml:"vm_json,omitempty" json:"vm_json"`

	DistroJSON RawBody `yaml:"distro_json,omitempty" json:"distro_json,omitempty"`

	BuilderJSON RawBody `yaml:"builder_json,omitempty" json:"builder_json,omitempty"`

	Engine string `yaml:"engine,omitempty" json:"engine,omitempty"`

	Rootful bool `yaml:"rootful,omitempty" json:"rootful,omitempty"`

	BootcImageRef string `yaml:"bootc_image_ref,omitempty" json:"bootc_image_ref,omitempty"`

	BuilderImageRef string `yaml:"builder_image_ref,omitempty" json:"builder_image_ref,omitempty"`

	OutputDir string `yaml:"output_dir,omitempty" json:"output_dir"`

	VmStateDir string `yaml:"vm_state_dir,omitempty" json:"vm_state_dir"`

	ExistingState *VmDeployState `yaml:"existing_state,omitempty" json:"existing_state,omitempty"`

	Force bool `yaml:"force,omitempty" json:"force,omitempty"`
}

#VmBuildReply is the resolveVmBuild reply (P8b-rest — the former "vm-build" host-builder is DELETED; candy/plugin-vm/vm_build_resolve.go resolves it plugin-side): everything the plugin needs to run the disk-build engine without importing the loader. VmJSON is the resolved+validated kind:vm entity (the #Vm-shaped value resolveVmViaPlugin already produces — opaque bytes of the same #Vm-shaped payload convention) so the plugin decodes it into its own spec.Vm rather than re-parsing uf.VM[entity] itself (which needs LoadUnified, a core Mechanism). DistroJSON/BuilderJSON carry the matched *DistroDef/*BuilderDef (bootstrap source only) — hand-written runtime types with no CUE def, so they ride as opaque RawBody too (the established idiom this file documents at the top). Engine/Rootful are the resolved runtime settings (ResolveRuntime) the engine needs to pick `podman` vs `sudo podman`. BootcImageRef/BuilderImageRef are PRE-RESOLVED (and, for the builder image, pre-built via `charly box build`) — both need the local podman-storage + project-config lookup a plugin cannot do (resolveBootcImageRef / ensureBuilderImageBuilt stay host-side). OutputDir/VmStateDir are the resolved per-entity paths (vmshared.VmDiskDir is ALREADY plugin-importable, but the host still resolves+creates VmStateDir since it also reads the existing ledger state below). ExistingState is the entity's persisted VmDeployState (#VmDeployState already has a CUE def — a typed embed, not opaque) so the plugin reuses the same instance-id / regenerates the seed ISO idempotently.

type VmBuildRequest

type VmBuildRequest struct {
	Box string `yaml:"box,omitempty" json:"box"`

	Size string `yaml:"size,omitempty" json:"size,omitempty"`

	RootSize string `yaml:"root_size,omitempty" json:"root_size,omitempty"`

	Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Transport string `yaml:"transport,omitempty" json:"transport,omitempty"`

	Console bool `yaml:"console,omitempty" json:"console,omitempty"`

	Force bool `yaml:"force,omitempty" json:"force,omitempty"`
}

#VmBuildRequest carries the `charly vm build` command flags (the former VmBuildCmd fields). candy/plugin-vm/vm_build_resolve.go resolves the kind:vm entity + the build vocabulary + the per-source-kind image refs into a #VmBuildReply envelope PLUGIN-SIDE (the former "vm-build" host-builder is DELETED — PREP+RESOLVE moved into the plugin, P8b-rest); the plugin's `charly vm build` command then runs the actual privileged-container / qemu-img / bootc-install / cloud-init disk-build ENGINE itself, exactly as candy/plugin-build's podman DRIVE runs behind HostBuild("buildengine-prep") (P8b) — the same inversion, applied to the VM disk-build engine. force skips the cloud_image content-freshness check, forcing a base-disk rebuild even when unchanged (P8b-rest: `--force` predates command:vm's P10 externalization but was dropped from this seam then — restored here since BuildCloudImage's force parameter is load-bearing).

type VmCharlyInstall

type VmCharlyInstall struct {
	// VmCharlyInstall has ONLY `strategy` (the vm-spec skill's url/checksum are
	// STALE — the Go struct dropped them). auto: scp host binary post-boot;
	// scp: explicit form; skip: user-managed.
	Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
}

type VmChecksum

type VmChecksum struct {
	Type string `yaml:"type,omitempty" json:"type,omitempty"`

	Value string `yaml:"value,omitempty" json:"value,omitempty"`
}

type VmCloudInit

type VmCloudInit struct {
	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`

	Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"`

	Locale string `yaml:"locale,omitempty" json:"locale,omitempty"`

	Users []VmCloudInitUser `yaml:"users,omitempty" json:"users,omitempty"`

	Package []string `yaml:"package,omitempty" json:"package,omitempty"`

	RunCmd []string `yaml:"runcmd,omitempty" json:"runcmd,omitempty"`

	BootCmd []string `yaml:"bootcmd,omitempty" json:"bootcmd,omitempty"`

	WriteFiles []VmCloudInitFile `yaml:"write_files,omitempty" json:"write_files,omitempty"`

	Network *VmCloudInitNetwork `yaml:"network,omitempty" json:"network,omitempty"`

	Mirrors *VmCloudInitMirrors `yaml:"mirrors,omitempty" json:"mirrors,omitempty"`

	CharlyInstall *VmCharlyInstall `yaml:"charly_install,omitempty" json:"charly_install,omitempty"`

	Extra string `yaml:"extra,omitempty" json:"extra,omitempty"`
}

--------------------------------------------------------------------------- cloud_init: VmCloudInit. CLOSED. Genuine passthroughs: extra (raw cloud-config string) and network.ethernets (network-config v2, map[string]map[string]any → {[string]: {[string]: _}}). ---------------------------------------------------------------------------

type VmCloudInitFile

type VmCloudInitFile struct {
	Path string `yaml:"path,omitempty" json:"path"`

	Content string `yaml:"content,omitempty" json:"content,omitempty"`

	Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`

	Perms string `yaml:"perms,omitempty" json:"perms,omitempty"`

	Encoding string `yaml:"encoding,omitempty" json:"encoding,omitempty"`
}

type VmCloudInitMirrors

type VmCloudInitMirrors struct {
	APT []string `yaml:"apt,omitempty" json:"apt,omitempty"`

	DNF []string `yaml:"dnf,omitempty" json:"dnf,omitempty"`

	Pacman []string `yaml:"pacman,omitempty" json:"pacman,omitempty"`
}

type VmCloudInitNetwork

type VmCloudInitNetwork struct {
	Version int `yaml:"version,omitempty" json:"version,omitempty"`

	// network-config v2 map[string]map[string]any — typed-open passthrough.
	Ethernets map[string]map[string]any `yaml:"ethernets,omitempty" json:"ethernets,omitempty"`
}

type VmCloudInitUser

type VmCloudInitUser struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Sudo bool `yaml:"sudo,omitempty" json:"sudo,omitempty"`

	Groups []string `yaml:"groups,omitempty" json:"groups,omitempty"`

	Shell string `yaml:"shell,omitempty" json:"shell,omitempty"`

	LockPasswd *bool `yaml:"lock_passwd,omitempty" json:"lock_passwd,omitempty"`
}

type VmDeployState

type VmDeployState struct {
	InstanceID string `yaml:"instance_id,omitempty" json:"instance_id,omitempty"`
	DiskPath   string `yaml:"disk_path,omitempty" json:"disk_path,omitempty"`
	SeedIso    string `yaml:"seed_iso,omitempty" json:"seed_iso,omitempty"`
	SSHPort    int    `yaml:"ssh_port,omitempty" json:"ssh_port,omitempty"`
	// PortForwards maps a guest port (string) to its auto-allocated host port for
	// each `auto:<guest>` network.port_forwards entry — persisted at vm-create so
	// the allocation is reused across create→deploy-add (the sibling of SSHPort).
	PortForwards            map[string]int          `yaml:"port_forwards,omitempty" json:"port_forwards,omitempty"`
	SSHUser                 string                  `yaml:"ssh_user,omitempty" json:"ssh_user,omitempty"`
	Backend                 string                  `yaml:"backend,omitempty" json:"backend,omitempty"`
	KeyInjectionResolved    *VmKeyInjectionResolved `yaml:"key_injection_resolved,omitempty" json:"key_injection_resolved,omitempty"`
	CharlyInstallStrategy   string                  `yaml:"charly_install_strategy,omitempty" json:"charly_install_strategy,omitempty"`
	CloudInitRenderedDigest string                  `yaml:"cloud_init_rendered_digest,omitempty" json:"cloud_init_rendered_digest,omitempty"`
	Snapshots               []VmSnapshotState       `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`
	Ephemeral               *EphemeralRuntime       `yaml:"ephemeral,omitempty" json:"ephemeral,omitempty"`
}

VmDeployState is the runtime state the vm lifecycle hook's PrepareVenue writes on first apply. Source: charly/deploy.go VmDeployState.

type VmDisplayEndpoint

type VmDisplayEndpoint struct {
	Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`

	IsSocket bool `yaml:"is_socket,omitempty" json:"is_socket,omitempty"`

	SocketPath string `yaml:"socket_path,omitempty" json:"socket_path,omitempty"`

	Host string `yaml:"host,omitempty" json:"host,omitempty"`

	Port int `yaml:"port,omitempty" json:"port,omitempty"`

	Password string `yaml:"password,omitempty" json:"password,omitempty"`

	TunnelNeeded bool `yaml:"tunnel_needed,omitempty" json:"tunnel_needed,omitempty"`
}

#VmDisplayEndpoint describes how to reach one graphics channel (SPICE or VNC) of a running VM. Shared by candy/plugin-vm's own DisplayEndpoint (a running-VM's resolved endpoint, kind="spice"| "vnc") and charly-core's decode of the resolve-spice/resolve-vnc RPC reply — ONE shape, R3.

type VmInstaller added in v0.2026241.2320

type VmInstaller struct {
	Username string `yaml:"username,omitempty" json:"username,omitempty"`

	Full_name string `yaml:"full_name,omitempty" json:"full_name,omitempty"`

	Email string `yaml:"email,omitempty" json:"email,omitempty"`

	// password is PLAINTEXT. The renderer crypt()s it before it reaches any template,
	// so the plaintext exists only in memory and never lands on the answers volume.
	Password string `yaml:"password,omitempty" json:"password,omitempty"`

	Password_hash string `yaml:"password_hash,omitempty" json:"password_hash,omitempty"`

	// disk is the target block device the installer wipes. It is the guest's view
	// (/dev/vda), not a host path.
	Disk string `yaml:"disk,omitempty" json:"disk,omitempty"`

	// encrypt requests full-disk encryption. NOTE: an encrypted unattended install is
	// not fully unattended — someone still types the LUKS passphrase at first boot —
	// and the passphrase is written in PLAINTEXT onto the answers volume, which makes
	// that volume a secret. Source it from the secrets backend, never a committed
	// literal.
	Encrypt bool `yaml:"encrypt,omitempty" json:"encrypt,omitempty"`

	Keyboard string `yaml:"keyboard,omitempty" json:"keyboard,omitempty"`

	Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"`

	Locale string `yaml:"locale,omitempty" json:"locale,omitempty"`

	Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`

	// ssh_authorized_key seeds the created account's ~/.ssh/authorized_keys. On a
	// distro whose installer honours it this is what makes the guest reachable at all
	// (a stock Omarchy install ships openssh with the service disabled and the port
	// closed), so charly defaults it to the public half of the per-VM generated key.
	Ssh_authorized_key []string `yaml:"ssh_authorized_key,omitempty" json:"ssh_authorized_key,omitempty"`

	// defer_provisioning installs with NO personal details, leaving the first person
	// to boot the machine to create their own user — the imaging-rig mode. Mutually
	// exclusive with the credential fields; enforced in OpValidate.
	Defer_provisioning bool `yaml:"defer_provisioning,omitempty" json:"defer_provisioning,omitempty"`

	// answer carries per-distro extras the common fields do not model (a Tailscale
	// auth key, a proxy, a licence). Typed-open by design.
	Answer map[string]string `yaml:"answer,omitempty" json:"answer,omitempty"`
}

#VmInstaller — the unattended-install ANSWERS for a `source.kind: iso` VM.

Distro-AGNOSTIC on purpose: every field here is a question EVERY installer asks. Distro-SPECIFIC extras go in `answer:` (typed-open — a distro's installer vocabulary is that distro's business, not vm.cue's), reachable from a #DistroInstaller template as {{index .Answers "key"}}. The FORMAT that renders these into files lives on the distro (#DistroInstaller), exactly as `init:` renders `service:`: archinstall JSON, kickstart, preseed, Subiquity autoinstall and AutoYaST are five real formats, each keyed by distro and by nothing else.

password ⊻ password_hash is enforced by the vm kind's OWN OpValidate, not by CUE — the same reason validateSourceDistro lives there: the host's value gate is closedness-only by design, and a disjunction over two optional strings cannot express "exactly one present".

type VmKeyInjection

type VmKeyInjection struct {
	SMBIOS string `yaml:"smbios,omitempty" json:"smbios,omitempty"`

	CloudInit string `yaml:"cloud_init,omitempty" json:"cloud_init,omitempty"`
}

type VmKeyInjectionResolved

type VmKeyInjectionResolved struct {
	SMBIOS    bool `yaml:"smbios" json:"smbios"`
	CloudInit bool `yaml:"cloud_init" json:"cloud_init"`
}

VmKeyInjectionResolved is the effective key-injection state. Source: charly/deploy.go VmKeyInjectionResolved.

type VmNetwork

type VmNetwork struct {
	Model string `yaml:"model,omitempty" json:"model,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode"`

	Bridge string `yaml:"bridge,omitempty" json:"bridge,omitempty"`

	MAC string `yaml:"mac,omitempty" json:"mac,omitempty"`

	// Each entry is "<host>:<guest>". The host side may be a fixed port OR the
	// literal `auto` sentinel (matching the pod `port: [auto]` word) — `auto`
	// auto-allocates a free host port at vm-create (persisted in vm_state,
	// reused across the create→deploy-add sequence), the sibling of ssh.port_auto.
	PortForwards []string `yaml:"port_forwards,omitempty" json:"port_forwards,omitempty"`
}

type VmPluginEnv

type VmPluginEnv struct {
	VmOp string `yaml:"vm_op,omitempty" json:"vm_op"`

	VmName string `yaml:"vm_name,omitempty" json:"vm_name,omitempty"`

	URI string `yaml:"uri,omitempty" json:"uri,omitempty"`

	Force bool `yaml:"force,omitempty" json:"force,omitempty"`

	DeleteDisk bool `yaml:"delete_disk,omitempty" json:"delete_disk,omitempty"`

	Snap *VmSnapInternalReq `yaml:"snap,omitempty" json:"snap,omitempty"`
}

#VmPluginEnv is the host→plugin env for an internal VM-resolution RPC (domain-state/ list-domains/resolve-spice/resolve-vnc/snapshot-internal). Matches the SUBSET of candy/plugin-vm's own (richer) internal vmEnv decode struct that charly-core's dispatch actually sends — plugin-vm's vmEnv carries an ADDITIONAL `create` field (the `charly vm create` CLI's own in-process construction, never sent by charly-core), which stays a plugin-local concern outside this shared def (containing *VmSpec/VmRuntimeParams, neither CUE-sourced yet). (A former `state_dir` field served ONLY the "qemu-shutdown" op, whose sole sender — charly/vm_qemu_client.go — was deleted in FLOOR-SLIM-proper Unit-8B alongside the arbiter's direct-QEMU stop path; the field was removed in the same cutover, confirmed zero remaining setters via `git grep 'VmPluginEnv{'` across charly/candy/sdk.)

type VmResolveInput

type VmResolveInput struct {
	Vm RawBody `yaml:"vm,omitempty" json:"vm"`
}

#VmResolveInput carries one opaque vm template body to project (Cutover L).

type VmResolveReply

type VmResolveReply struct {
	Resolved *ResolvedVm `yaml:"resolved,omitempty" json:"resolved,omitempty"`
}

#VmResolveReply wraps the resolved vm value envelope (#ResolvedVm lives in vm.cue).

type VmResolveResult

type VmResolveResult struct {
	Endpoint VmDisplayEndpoint `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`

	Error string `yaml:"error,omitempty" json:"error,omitempty"`

	TunnelTarget string `yaml:"tunnel_target,omitempty" json:"tunnel_target,omitempty"`
}

#VmResolveResult decodes a resolve-spice/resolve-vnc reply.

type VmSize

type VmSize = string

--- named-pattern scalars (string) ---

type VmSnapInternalReq

type VmSnapInternalReq struct {
	SnapOp string `yaml:"snap_op,omitempty" json:"snap_op"`

	VmName string `yaml:"vm_name,omitempty" json:"vm_name"`

	Opts *VmSnapshotCreateOpts `yaml:"opts,omitempty" json:"opts,omitempty"`

	Entry *VmSnapshotEntry `yaml:"entry,omitempty" json:"entry,omitempty"`

	OutPath string `yaml:"out_path,omitempty" json:"out_path,omitempty"`
}

#VmSnapInternalReq is the snapshot-internal op payload (create/delete/revert/promote/ create-external/delete-external/revert-external), threaded via #VmPluginEnv.snap.

type VmSnapshot

type VmSnapshot struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Quiesce bool `yaml:"quiesce,omitempty" json:"quiesce,omitempty"`

	From string `yaml:"from,omitempty" json:"from,omitempty"`
}

type VmSnapshotCreateOpts

type VmSnapshotCreateOpts struct {
	VmName string `yaml:"vm_name,omitempty" json:"vm_name"`

	SnapName string `yaml:"snap_name,omitempty" json:"snap_name"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Quiesce bool `yaml:"quiesce,omitempty" json:"quiesce,omitempty"`

	LibvirtBackend string `yaml:"libvirt_backend,omitempty" json:"libvirt_backend,omitempty"`
}

#VmSnapshotCreateOpts parameterizes the creation of a snapshot — the host-resolved payload for the snapshot-internal "create"/"create-external" ops.

type VmSnapshotDecl

type VmSnapshotDecl = VmSnapshot

--- candy sub-types ---

type VmSnapshotEntry

type VmSnapshotEntry struct {
	Name string `yaml:"name,omitempty" json:"name"`

	Mode string `yaml:"mode,omitempty" json:"mode"`

	LibvirtName string `yaml:"libvirt_name,omitempty" json:"libvirt_name,omitempty"`

	DiskPath string `yaml:"disk_path,omitempty" json:"disk_path,omitempty"`

	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	Created string `yaml:"created,omitempty" json:"created,omitempty"`

	Parent string `yaml:"parent,omitempty" json:"parent,omitempty"`

	Refcount int `yaml:"refcount,omitempty" json:"refcount"`

	Quiesced bool `yaml:"quiesced,omitempty" json:"quiesced,omitempty"`
}

#VmSnapshotEntry is one snapshot record (mirrors sdk/vmshared's SnapshotEntry, the on-disk registry shape) — carried on delete/revert/promote ops so the plugin's go-libvirt call has the full record without re-reading the host-side registry itself.

type VmSnapshotPolicy added in v0.2026244.920

type VmSnapshotPolicy struct {
	OnFinalize string `yaml:"on_finalize,omitempty" json:"on_finalize,omitempty"`

	ResetBeforeCheck string `yaml:"reset_before_check,omitempty" json:"reset_before_check,omitempty"`

	Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`

	Consistent bool `yaml:"consistent,omitempty" json:"consistent,omitempty"`

	KeepVenue bool `yaml:"keep_venue,omitempty" json:"keep_venue,omitempty"`
}

#VmSnapshotPolicy — the check-bed snapshot-anchoring policy (§5.3.1). Extends the existing from_snapshot: (boot from a backing chain) with capture-and-reset semantics for check runs: capture at install finalize (on_finalize), revert before every check run (reset_before_check), optionally quiesced (consistent), internal or external mode, and keep_venue for batch loops.

type VmSnapshotState

type VmSnapshotState struct {
	Name        string `yaml:"name" json:"name"`
	Mode        string `yaml:"mode" json:"mode"`
	LibvirtName string `yaml:"libvirt_name,omitempty" json:"libvirt_name,omitempty"`
	DiskPath    string `yaml:"disk_path,omitempty" json:"disk_path,omitempty"`
	Description string `yaml:"description,omitempty" json:"description,omitempty"`
	Created     string `yaml:"created,omitempty" json:"created,omitempty"`
	Parent      string `yaml:"parent,omitempty" json:"parent,omitempty"`
	Refcount    int    `yaml:"refcount" json:"refcount"`
}

VmSnapshotState mirrors one snapshot in the charly.yml vm_state record. Source: charly/deploy.go VmSnapshotState.

type VmSource

type VmSource struct {
	Kind             string     `yaml:"kind" json:"kind"`
	URL              string     `yaml:"url,omitempty" json:"url,omitempty"`
	Checksum         VmChecksum `yaml:"checksum,omitempty" json:"checksum,omitempty"`
	Cache            string     `yaml:"cache,omitempty" json:"cache,omitempty"`
	BaseUser         string     `yaml:"base_user,omitempty" json:"base_user,omitempty"`
	Box              string     `yaml:"box,omitempty" json:"box,omitempty"`
	Transport        string     `yaml:"transport,omitempty" json:"transport,omitempty"`
	Rootfs           string     `yaml:"rootfs,omitempty" json:"rootfs,omitempty"`
	RootSize         string     `yaml:"root_size,omitempty" json:"root_size,omitempty"`
	KernelArgs       string     `yaml:"kernel_args,omitempty" json:"kernel_args,omitempty"`
	FromVm           string     `yaml:"from_vm,omitempty" json:"from_vm,omitempty"`
	FromSnapshot     string     `yaml:"from_snapshot,omitempty" json:"from_snapshot,omitempty"`
	CloudInitClean   bool       `yaml:"cloud_init_clean,omitempty" json:"cloud_init_clean,omitempty"`
	LibvirtName      string     `yaml:"libvirt_name,omitempty" json:"libvirt_name,omitempty"`
	DiskPath         string     `yaml:"disk_path,omitempty" json:"disk_path,omitempty"`
	DiskFormat       string     `yaml:"disk_format,omitempty" json:"disk_format,omitempty"`
	AdoptedAt        string     `yaml:"adopted_at,omitempty" json:"adopted_at,omitempty"`
	LastSyncedAt     string     `yaml:"last_synced_at,omitempty" json:"last_synced_at,omitempty"`
	Builder          string     `yaml:"builder,omitempty" json:"builder,omitempty"`
	BuilderImage     string     `yaml:"builder_image,omitempty" json:"builder_image,omitempty"`
	Distro           string     `yaml:"distro,omitempty" json:"distro,omitempty"`
	Package          []string   `yaml:"package,omitempty" json:"package,omitempty"`
	BootstrapArch    string     `yaml:"bootstrap_arch,omitempty" json:"bootstrap_arch,omitempty"`
	BootstrapVariant string     `yaml:"bootstrap_variant,omitempty" json:"bootstrap_variant,omitempty"`

	// iso arm. Installer carries the unattended-install ANSWERS; the distro owns the
	// FORMAT that renders them (Distro.Installer / #DistroInstaller).
	Installer *VmInstaller `yaml:"installer,omitempty" json:"installer,omitempty"`
}

VmSource is the discriminated-union source for a VM disk image (Kind selects the active branch: cloud_image / bootc / clone / imported / bootstrap).

type VmSpec

type VmSpec = Vm

--- top-level entity types ---

type VmSsh added in v0.2026232.520

type VmSsh struct {
	User         string          `yaml:"user,omitempty" json:"user,omitempty"`
	Port         int             `yaml:"port,omitempty" json:"port,omitempty"`
	PortAuto     bool            `yaml:"port_auto,omitempty" json:"port_auto,omitempty"`
	KeySource    string          `yaml:"key_source,omitempty" json:"key_source,omitempty"`
	KeyInjection *VmKeyInjection `yaml:"key_injection,omitempty" json:"key_injection,omitempty"`
}

VmSsh is the guest SSH access config (port XOR port_auto is a Go cross-field rule). KeyInjection references the generated VmKeyInjection.

type VmVariant added in v0.2026244.920

type VmVariant struct {
	Cpus int `yaml:"cpus,omitempty" json:"cpus,omitempty"`

	Memory VmSize `yaml:"memory,omitempty" json:"memory,omitempty"`

	Video string `yaml:"video,omitempty" json:"video,omitempty"`

	Gpu *struct {
		Hostdev *string `yaml:"hostdev,omitempty" json:"hostdev,omitempty"`

		Vendor *string `yaml:"vendor,omitempty" json:"vendor,omitempty"`
	} `yaml:"gpu,omitempty" json:"gpu,omitempty"`

	Display string `yaml:"display,omitempty" json:"display,omitempty"`

	Devices []string `yaml:"devices,omitempty" json:"devices,omitempty"`

	Attachments []string `yaml:"attachments,omitempty" json:"attachments,omitempty"`
}

#VmVariant — a named VM-config override that boots the SAME golden disk with a different shape. Only VM-shape fields are legal (cpus/memory/video/gpu/ display/devices/attachments); any change to source: or disk identity is rejected at validate time, because the disk comes exclusively from the shared snapshot chain.

type VolumeMount

type VolumeMount struct {
	VolumeName string `yaml:"volume_name,omitempty" json:"volume_name,omitempty"`

	ContainerPath string `yaml:"container_path,omitempty" json:"container_path,omitempty"`
}

#VolumeMount — a resolved named-volume mount (charly-<deploy>-<name> → container path). In-memory only (never marshaled) → tags wire-irrelevant. deploykit.VolumeMount aliases this.

type VolumeYAML

type VolumeYAML = CandyVolume

--- candy sub-types ---

type WalkSeams

type WalkSeams struct {
	// Parser is the per-document parse (the host passes the registered DocParser).
	Parser DocParser
	// Boundary runs at each PROJECT boundary (the root file AND each namespace root) BEFORE that
	// boundary's documents parse: the host does the parse pre-scan + connect-declared-kind-plugins
	// side effects (registry mutation). data = the boundary file bytes.
	Boundary func(dir string, data []byte) error
	// Threaded returns the current registry-derived kind-recognition snapshot. Called fresh per
	// document parse (the host's loaderThreaded()).
	Threaded func() Threaded
	// ResolveRef resolves an import ref (local path OR remote "@host/org/repo[/sub]:ver") to a
	// stable cache KEY + a concrete on-disk PATH. The host owns remote fetch + cache + auto-migration.
	ResolveRef func(ref, baseDir string) (key, path string, err error)
	// GateDoc runs the host #NodeDoc CUE validate-before-execute gate on one raw document's bytes.
	GateDoc func(label string, raw []byte) error
	// RepoIdentity returns the canonical repo identity of an import ref (for the cycle-break), or ""
	// (the host's nsRepoIdentity). Empty → version-keyed fallback.
	RepoIdentity func(ref, baseDir string) string
}

WalkSeams is the set of host-supplied callbacks the whole-project WALK needs for everything registry-coupled or host-coupled — the host builds this value and hands it to the registered ProjectWalker; the walk mechanism (sdk/loaderkit.Walk) calls each seam and never does the coupled work itself (boundary law clause D: the walk consults host-threaded DATA/mechanisms, never the provider registry directly).

Source Files

Jump to

Keyboard shortcuts

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