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: charly/capabilities.go + charly/labels.go. The CLI read-side probe is charly box labels <ref> (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 Capabilities type alias
Section titled “The Capabilities type alias”// charly/capabilities.go:29type Capabilities = BoxMetadataCapabilities is a Go type alias — not a separate struct. Every existing consumer that holds an *BoxMetadata (there are many) transparently participates in the capabilities contract. New code (K8s generator, charly bundle from-box) uses the canonical Capabilities name for readability. Aliasing (rather than wrapping) means there’s exactly one place fields are defined: BoxMetadata in charly/labels.go.
Why this matters: adding a field anywhere in 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”charly/capabilities.go:35 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 charly/generate.go’s NewGenerator). 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: resolveEntrypointFromMeta (container entrypoint) and resolveInitDefFromMeta (in-container service-management) read this label FIRST, falling back to the legacy wellKnownInitDefs registry only for pre-init_def-label images. 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 (charly/shellcollect.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 bundle 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 sdk/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”charly/capabilities_test.go:TestCapabilityLabelCompleteness runs on every go test ./... invocation. It uses reflect.TypeOf(BoxMetadata{}) to enumerate every exported field and fails if any field is missing from CapabilityLabelMap:
// charly/capabilities.go:143func checkCapabilityLabelCompleteness() error { rt := reflect.TypeOf(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:
- Add the field to
BoxMetadataincharly/labels.gowith a JSON tag. - Add the label const (
LabelFoo = "ai.opencharly.foo") next to the other label consts. - Add the
CapabilityLabelMapentry:"Foo": LabelFoo. - Emit + parse the label in
WriteLabels(deploykit, viawriteJSONLabel) /ExtractMetadata. 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:
// charly/labels.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 bundle from-box (see below) reconstructs the full deploy surface from OCI labels alone. A names-only Service label would leave deploy-time K8s 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 K8s Kustomize generator (see /charly-kubernetes:kubernetes) reads from it without touching the source repo.
Source-less deploy: charly bundle from-box
Section titled “Source-less deploy: charly bundle from-box”CapabilitiesFromLabels(engine, imageRef) at charly/capabilities.go:166 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 *Capabilities, and every downstream consumer (deploy target, K8s generator, quadlet generator) reads from that struct.
caps, err := CapabilitiesFromLabels("podman", "ghcr.io/opencharly/fedora-coder:latest")// caps.Service[0].Kind == "eventlistener" works — no source repo neededThis is what enables the “K8s 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 (documented in charly/capabilities.go:17-22) is to split BoxConfig into three discriminated sections:
box.build:— Containerfile inputs (base image, layers, distro/builder selection). Consumed only bycharly box build.box.capabilities:— the runtime contract documented here. Emitted as OCI labels.box.deploy:— target-specific defaults (K8s storage class, container-target port defaults). Consumed bycharly bundle add.
Today these co-exist in a single BoxConfig. The Capabilities alias is the stepping stone — once the schema split lands, Capabilities will point at the capabilities: subsection directly instead of aliasing the whole struct. The CapabilityLabelMap completeness test will keep the contract honest across the migration.
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.
Adding a new OCI label — checklist
Section titled “Adding a new OCI label — checklist”- Add the const to
charly/labels.go(LabelFoo = "ai.opencharly.foo"). - Add the field to
BoxMetadatawith a matching JSON tag. - Add the
CapabilityLabelMapentry:"Foo": LabelFoo. - Populate it in
WriteLabels(deploykit label emission at build time, viawriteJSONLabelfor struct/list values). - Parse it in
ExtractMetadata(label read-back at deploy time). - If the field is a struct/list, route it through
writeJSONLabelfor consistent encoding (see howLabelService,LabelDescription,LabelVolumedo it). go test ./...—TestCapabilityLabelCompletenesspasses.- Update
/charly-internals:capabilities(this skill) with the new entry in the key-labels table.
See Also
Section titled “See Also”/charly-image:image— user-facingcandy:image entries (withbase:/from:) incharly.yml/charly-image:layer— user-facingcandy:authoring, includingservice:which feedsLabelService/charly-core:deploy—charly bundle add/from-image/synccommands/charly-kubernetes:kubernetes— K8s deploy target that readsLabelServiceto generate Kustomize/charly-check:check— three-sectionLabelDescription(candy/box/deploy) — same label-contract pattern/charly-build:migrate—charly migrate— emits the schema that populates these labels/charly-internals:go— Go architecture overview,LoadUnified,parseCandyYAML/charly-internals:install-plan— internal IR shared across build and deploy pipelines