Skip to content

capabilities

Recipe card from the charly-internals plugin (Development — contributor internals).

Capabilities — the OCI-label runtime contract

Section titled “Capabilities — the OCI-label runtime contract”

Every opencharly image carries a complete snapshot of “what can this image do, what does it need, what does it provide” as ai.opencharly.* OCI labels. This skill documents the contract, the single Go type behind it, the completeness test that keeps it honest, and the source-less deploy path it enables.

Source of truth: sdk/deploykit/capabilities.go (the CapabilityLabelMap + CapabilitiesFromLabels + the completeness test) + spec/spec/label_consts.go (the label name constants) + spec/spec/cue_types_gen.go (BoxMetadata, CUE-generated) + spec/container/box_metadata_coneb.go (ExtractMetadata/InspectLabels). charly/capabilities.go and charly/labels.go no longer exist — both relocated out of core (FLOOR-SLIM Unit 4 / #55 coneB); charly box labels <ref> is now candy/plugin-box/box.go’s dispatchLabels, which reads ai.opencharly.* labels via ExtractMetadata (whole contract sorted; --format <key> for one raw value with a non-zero exit when absent; --all for non-charly labels too) — the charly-native R8 artifact check. See also /charly-internals:go for the broader Go architecture and /charly-internals:install-plan for how this feeds the build/deploy IR.

Boundary-law note. The ai.opencharly.* label contract is a resolved-envelope contract, not a typed-kind one (/charly-internals:plugin “The kernel/plugin boundary law”): an image advertises what it provides / needs / can do as generic label DATA that any consumer reads by key — the kernel never imports a concrete kind’s spec.<Kind> type to interpret a label. A new capability field is generic label vocabulary (Data), gated by TestCapabilityLabelCompleteness; it never adds a per-kind branch.

The BoxMetadata type — no more Capabilities alias

Section titled “The BoxMetadata type — no more Capabilities alias”

There is no Capabilities name anymore. The former charly-core type Capabilities = BoxMetadata was ZERO-ALIASES residue per the kernel/plugin boundary law (an alias is always an R-item regardless of what it aliases) — deleted; every consumer references spec.BoxMetadata directly (CUE-generated, spec/spec/cue_types_gen.go). This doc keeps the historical name in prose below only where it clarifies intent (“the capabilities contract”); the Go type to reach for is always spec.BoxMetadata.

Why this matters: adding a field anywhere in spec.BoxMetadata automatically becomes part of the capabilities contract, which means it MUST have a CapabilityLabelMap entry. Forget the entry and CI blocks the PR.

CapabilityLabelMap — field → OCI-label name

Section titled “CapabilityLabelMap — field → OCI-label name”

sdk/deploykit/capabilities.go names every label that participates in the contract. Entry grouping (identity / account / ports / security / networking / env / engine+init / distro+builder / hooks+vm / skills / data / dependency-graph / tests) mirrors the BoxMetadata field ordering so the map reads as a spec of the on-disk format.

Key entries:

Field Label const What it stores
Version LabelVersion (ai.opencharly.version) The image’s content-derived EffectiveVersion — its dedicated version: if set, else the highest layer version: across the chain (deploykit.ComputeEffectiveVersions, called from candy/plugin-build/resolve.go’s build-engine RESOLVE). NOT the per-build tag; stable when no layer changed. Short-name resolution + charly clean retention prefer this label over the tag. Also the “is this an charly image?” presence sentinel (ExtractMetadata returns nil when empty).
Service LabelService (ai.opencharly.service) Structured JSON array of CapabilityService — not just names. 23 per-entry fields including kind, events, auto_start, start_retries, priority, init, layer. See “LabelService” below.
Init LabelInit Init system name (supervisord / systemd / none).
InitDef LabelInitDef (ai.opencharly.init_def) *CapabilityInitDef — the build-resolved runtime subset of the init: vocabulary entry: entrypoint, fallback_entrypoint, management_tool, management_commands. Makes the init system TRUE single-source: kit.ResolveEntrypointFromMeta (container entrypoint) and resolveInitDefFromMeta (in-container service-management) read this label as their ONLY source. A frozen wellKnownInitDefs registry used to back them for pre-init_def-label images; it was deleted (it was triplicated across sdk/kit, plugin-pod and plugin-deploy-pod, and frozen at supervisord + systemd, so it could never answer for a third init system such as openrc). An image without the label now errors, naming the rebuild, rather than resolving a guessed management CLI. Because the contract travels in the label, init systems declared ONLY in the embedded vocabulary now work at runtime too.
ServiceNames LabelInit Per-init active-name list; baked alongside LabelInit for CLI ergonomics (e.g., charly service status).
Description LabelDescription (ai.opencharly.description) Three-section {candy, box, deploy} LabelDescriptionSet — the self-description baked into the image. Each LabeledDescription carries a Description string plus a Plan []Step list (each step is an intent keyword run:/check:/agent-run:/agent-check: + an inline Op); the deterministic check: steps are the acceptance checks consumed by charly check live / charly check box. See /charly-check:check.
CheckLevel LabelCheckLevel (ai.opencharly.check_level) The per-box acceptance-depth rung (none / build / noagent (default) / agent) gating how deep charly check run <bed> drives acceptance.
Shell LabelShell Three-section {candy, box, deploy} JSON shell-init manifest. Each entry carries an Origin (candy name / “box”), an ID for overlay keying, an optional Generic body (intrinsic init + path_append) and a per-shell ByShell map (bash/zsh/fish/sh sub-blocks). CollectShell (sdk/deploykit/shell_collect.go) populates the Candy + Box sections at charly box build time; ExtractMetadata parses the label at deploy time. Consumed by charly box inspect and charly fleet from-box. The Deploy section is now permanently empty: the validation-correctness batch retired the deploy-scope shell: overlay AUTHORING FIELD itself (#Deploy.shell/#DeployShellOverlay deleted from spec/schema/deploy.cue) — a config still carrying it is migrated away by charly migrate’s strip-deploy-shell-overlay step — so it is no longer merely “parsed but unapplied” (as an earlier pass of this doc described a transitional state); the field cannot be authored at all anymore. See /charly-image:layer “Shell Init Surface”.
EnvProvide / MCPProvide LabelEnvProvide / LabelMCPProvide Cross-container discovery: what env vars / MCP servers this image advertises to pod peers.
EnvRequire / MCPRequire LabelEnvRequire / LabelMCPRequire What this image needs from peers — validated at charly config time.

TestCapabilityLabelCompleteness — the guardrail

Section titled “TestCapabilityLabelCompleteness — the guardrail”

sdk/deploykit/capabilities_test.go:TestCapabilityLabelCompleteness runs on every go test ./... invocation. It uses reflect.TypeOf(spec.BoxMetadata{}) to enumerate every exported field and fails if any field is missing from CapabilityLabelMap:

sdk/deploykit/capabilities.go
func checkCapabilityLabelCompleteness() error {
rt := reflect.TypeOf(spec.BoxMetadata{})
var missing []string
for i := 0; i < rt.NumField(); i++ {
name := rt.Field(i).Name
if _, ok := CapabilityLabelMap[name]; !ok {
missing = append(missing, name)
}
}
// ...
}

This is the enforcement mechanism that keeps the OCI-label contract and the Go struct in sync. Workflow for adding a capability:

  1. Add the field to BoxMetadata in spec/schema/boxmetadata.cue (CUE is the single source of truth; task cue:gen regenerates spec/spec/cue_types_gen.go).
  2. Add the label const (LabelFoo = "ai.opencharly.foo") in spec/spec/label_consts.go.
  3. Add the CapabilityLabelMap entry: "Foo": LabelFoo.
  4. Emit + parse the label in WriteLabels (deploykit, via writeJSONLabel) / ExtractMetadata.
  5. go test ./... passes.

Skip step 3 and the test fails with BoxMetadata fields without CapabilityLabelMap entry: [Foo].

LabelService — structured per-entry service data

Section titled “LabelService — structured per-entry service data”

The services label is a full structured round-trip — not a flat list of names:

// spec/schema/boxmetadata.cue → spec/spec/cue_types_gen.go (CapabilityService struct, paraphrased)
type CapabilityService struct {
Name string
Scope string // system / user
Enable bool
UsePackaged string // name of distro-shipped unit to reuse
Exec string
Env map[string]string
Restart string
WorkingDirectory string
User string
After []string
Before []string
Stdout string
StopTimeout string
Kind string // "program" (default) | "eventlistener"
Events string // required when Kind == "eventlistener"
AutoStart *bool // three-state; supervisord autostart=
StartRetries int
StartSec int
StopSignal string
ExitCode string
Priority int
Init string // which init owns it (supervisord / systemd)
Layer string // source layer name
}

Why this matters: charly fleet from-box (see below) reconstructs the full deploy surface from OCI labels alone. A names-only Service label would leave deploy-time Kubernetes manifest generation blind to whether a process needs start_retries: 3 or is an eventlistener. The structured label carries every supervisord directive faithfully, and the Kubernetes Kustomize generator (see /charly-kubernetes:kubernetes) reads from it without touching the source repo.

deploykit.CapabilitiesFromLabels(engine, imageRef) (sdk/deploykit/capabilities.go) is the source-less entry point: given an engine + image ref, it runs ExtractMetadata (which pulls labels via podman inspect / docker inspect), returns a fully-populated *spec.BoxMetadata, and every downstream consumer (deploy target, Kubernetes generator, quadlet generator) reads from that struct. candy/plugin-fleet/deploy_from_box.go is charly fleet from-box’s own caller.

caps, err := deploykit.CapabilitiesFromLabels("podman", "ghcr.io/opencharly/fedora-coder:latest")
// caps.Service[0].Kind == "eventlistener" works — no source repo needed

This is what enables the “Kubernetes deploy without access to charly.yml” invariant: a Kustomize overlay can be generated from a published image alone, for dev/staging/prod clusters that never see the build repo.

Three-layer image architecture (planned schema split)

Section titled “Three-layer image architecture (planned schema split)”

The long-term direction (aspirational — not yet started in code or schema) is to split BoxConfig into three discriminated sections:

  • box.build: — Containerfile inputs (base image, layers, distro/builder selection). Consumed only by charly box build.
  • box.capabilities: — the runtime contract documented here. Emitted as OCI labels.
  • box.deploy: — target-specific defaults (Kubernetes storage class, container-target port defaults). Consumed by charly fleet add.

Today these co-exist in a single BoxConfig. The CapabilityLabelMap completeness test will keep the contract honest across the migration (once the schema split lands, spec.BoxMetadata will project the capabilities: subsection directly instead of the whole struct).

See /charly-image:image for current user-facing structure and /charly-build:migrate for the one-shot charly migrate converter that emits the new schema when the split lands.

  1. Add the const to spec/spec/label_consts.go (LabelFoo = "ai.opencharly.foo").
  2. Add the field to BoxMetadata (spec/schema/boxmetadata.cue, CUE-generated) with a matching JSON tag.
  3. Add the CapabilityLabelMap entry: "Foo": LabelFoo.
  4. Populate it in WriteLabels (deploykit label emission at build time, via writeJSONLabel for struct/list values).
  5. Parse it in ExtractMetadata (label read-back at deploy time).
  6. If the field is a struct/list, route it through writeJSONLabel for consistent encoding (see how LabelService, LabelDescription, LabelVolume do it).
  7. go test ./...TestCapabilityLabelCompleteness passes.
  8. Update /charly-internals:capabilities (this skill) with the new entry in the key-labels table.