Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

The default capOS boot manifest (system.cue at the repo root) is layered on a shared scaffold in cue/defaults/defaults.cue. Operators can extend it without forking either file by dropping a system.local.cue overlay next to system.cue. The overlay is gitignored, so each developer/host can carry their own extensions without conflicting with git pull.

This document is the current operator-facing design for the configuration surface. The historical proposal and closeout rationale live in System Configuration and Operator Extensibility.

How the layering works

mkmanifest --package capos system.cue manifest.bin invokes cue export .:capos --out json against the repo root. CUE’s package mode unifies every non-hidden .cue file in that directory that declares package capos — currently system.cue (committed) and any system.local.cue (gitignored) the operator drops in. The shared scaffold is imported by system.cue:

import defaults "capos.local/cue/defaults"

#Manifest (the value system.cue exports) inherits all defaults from defaults.#DefaultSystem, then applies any operator overrides declared in system.local.cue. The kernel decoder reads concrete fields at the document root (schemaVersion, binaries, initConfig, kernelParams); #Manifest is documentation-only.

The decoder rejects any other top-level field name with a typed error. For an unknown field named kernelParameters the rendered message is:

unknown field `kernelParameters` at $; expected one of `schemaVersion`, `binaries`, `initConfig`, or `kernelParams`

CUE definitions (#Foo) and hidden fields (_foo) are stripped by cue export and never reach the decoder, so this only fires when the manifest projects an unintended visible name onto the document root — a typo such as kernelParameters: … instead of kernelParams: …, or a stale overlay field that was renamed in the defaults package. Fix it by renaming the projected field to one of the four accepted names, by moving the value under kernelParams.…, or by hiding the auxiliary value with a _/# prefix.

Quick start

Copy the committed example and edit:

cp system.local.cue.example system.local.cue
$EDITOR system.local.cue
make run

The Makefile picks up the new file automatically — no flag, no include line. make re-evaluates the manifest because system.local.cue is a prerequisite of the manifest rule.

Common Overlay Tasks

The examples below are complete system.local.cue fragments for common local configuration changes. Each fragment is intended to be copied as a starting point and adjusted before running make run.

Override the MOTD

package capos

#Manifest: kernelParams: motd: """
	hello, capOS dev box.
	type 'login' to authenticate.
	"""

The defaults package declares motd: string | *"...", so a concrete overlay value wins under CUE unification (a more concrete value is strictly more specific than a default).

The system hostname is set the same way via kernelParams.hostname (defaults to capos); it is served by SystemInfo.hostname and shown by the shell hostname command. Bootstrap validation rejects whitespace, control characters, and values longer than 255 bytes.

#Manifest: kernelParams: hostname: "web-01"

Add an authorized SSH key for the host operator

The default manifest declares a single host-operator seed account with the canonical 32-byte principal id local-operator-principal-default. Bind any number of authorized keys to that principal:

package capos

#Manifest: extraAuthorizedSshKeys: [{
	keyId:                "host-laptop-ed25519-2026-04"
	principalId:          "local-operator-principal-default"
	algorithm:            "ssh-ed25519"
	publicKey:            "<32-byte ed25519 public key as ASCII hex>"
	fingerprintSha256:    "<32-byte SHA-256 of the public key as ASCII hex>"
	allowedShellProfiles: ["operator"]
	source:               "manifest"
	comment:              "host laptop"
}]

Convert an existing ~/.ssh/id_ed25519.pub line to the manifest hex fields (Ed25519 example):

# extract the base64-encoded SSH wire format and decode the embedded key
ssh-keygen -e -m PKCS8 -f ~/.ssh/id_ed25519.pub | \
	openssl pkey -pubin -outform DER 2>/dev/null | \
	tail -c 32 | xxd -p -c 64
# fingerprintSha256 — SHA-256 over the same 32-byte raw public key:
ssh-keygen -e -m PKCS8 -f ~/.ssh/id_ed25519.pub | \
	openssl pkey -pubin -outform DER 2>/dev/null | \
	tail -c 32 | sha256sum | awk '{print $1}'

Use the printed hex as the publicKey and fingerprintSha256 strings.

The proposal explicitly avoids auto-ingesting ~/.ssh/*.pub from the Makefile. Manual conversion gives the operator control over which keys are trusted by the boot manifest.

Add a non-operator principal

The single-account-multi-auth invariant fixes the host operator at kind: "operator"; slice 2 rejects manifests with multiple operator seeds. Additional principals must use kind: "guest" or kind: "service":

package capos

#Manifest: extraSeedAccounts: [{
	name:            "kiosk-guest"
	displayName:     "Kiosk Guest"
	principalId:     "kiosk-guest-principal-32-bytes-x" // exactly 32 bytes
	kind:            "guest"
	credentialRefs:  []
	resourceProfile: "operator-default"
}]

Each seed account’s principalId must be unique and exactly 32 bytes; each must reference an existing resourceProfile (either operator-default from the defaults package or one declared in extraResourceProfiles).

Add a custom resource profile

package capos

#Manifest: extraResourceProfiles: [{
	name:                      "kiosk-guest-profile"
	homeQuotaBytes:            0
	tempQuotaBytes:            1048576
	processLimit:              2
	threadLimit:               4
	capLimit:                  24
	memoryCommitLimitBytes:    16777216
	frameGrantLimitPages:      64
	endpointQueueLimit:        8
	inFlightCallLimit:         4
	ringScratchLimitBytes:     16384
	logQuotaBytesPerWindow:    32768
	networkProfile:            "none"
	cpuBudgetUsPerWindow:      10000
	cpuWindowUs:               100000
	timerWaiterLimit:          2
	launcherProfile:           "bootstrap-guest"
}]

Reference the profile name from extraSeedAccounts[].resourceProfile.

Add a binary and an init-launched service

The defaults package exposes extraBinaries and extraServices hooks. The first embeds an additional binary into manifest.bin; the second appends an entry onto initConfig.services so init launches it after the base service graph. Build the binary as part of the operator workflow — the default Make targets only build the binaries already listed in the defaults package.

package capos

#Manifest: extraBinaries: [{
	name: "site-monitor"
	path: "demos/target/x86_64-unknown-capos/release/capos-demo-site-monitor"
}]

#Manifest: extraServices: [{
	name:    "site-monitor"
	binary:  "site-monitor"
	restart: "never"
	caps: [{
		name: "console"
		source: kernel: "console"
	}, {
		name: "timer"
		source: kernel: "timer"
	}],
}]

extraServices is concatenated onto _baseServices (base-first, then operator-extra), so the operator service starts after the defaults’ chat server, remote-session gateway, and shell are launched.

Override the console password verifier

The defaults package ships a development-only Argon2id PHC for the plaintext “capos”. Any non-research deployment should mint a fresh verifier and override it:

package capos

#Manifest: kernelParams: consolePasswordVerifierPhc:
	"$argon2id$v=19$m=19456,t=2,p=1$<salt-base64>$<hash-base64>"

Generate a verifier with the standalone argon2 tool (argon2 "<salt>" -id -t 2 -m 19 -p 1 -e) or from any Argon2id implementation that emits a PHC string with m=19456,t=2,p=1. The canonical 32-byte local-operator-principal-default operator principal id is unchanged; only the verifier rotates.

Host-user injection (@tag(user))

make run exports CAPOS_CUE_USER=$(USER), and mkmanifest forwards it as --inject user=.... When CAPOS_CUE_DISPLAY_NAME is unset, mkmanifest derives displayName from the same account’s first GECOS/comment field in /etc/passwd and forwards it as --inject displayName=.... If the passwd comment is unavailable or empty, displayName falls back to the account name. Other Make targets leave the structured tag variables unset, so untagged system.cue keeps the canonical operator account name. Focused demo and smoke manifests pin their own demo fixtures. The audit-correlatable principalId is fixed to the canonical 32-byte value regardless of host user, so audit history is stable across $USER changes.

mkmanifest also keeps the generic CAPOS_CUE_TAGS comma-separated escape hatch for additional key=value tags. The Makefile sets the structured variables target-scoped to make run only:

run: CAPOS_CUE_USER = $(USER)

Set additional tags via make USER=alice CAPOS_CUE_DISPLAY_NAME='Alice Smith' CAPOS_CUE_TAGS=region=eu-west run or by passing --tag key=value to mkmanifest directly. system.cue consumes user and displayName today; user must be a valid manifest seed account name. Future tags can carry hostname, locale, or other build-environment-derived values without adding new mechanisms.

Tools-root cache

CAPOS_TOOLS_ROOT defaults to $HOME/.capos-tools. The pinned toolchain (capnp, cue, mdbook, typst, limine) lives under that path so multiple capOS clones share a single download. Override with CAPOS_TOOLS_ROOT=/path/to/cache make ... for non-default placement. The Makefile and mkmanifest’s expected_cue_path follow the same default; mismatched CAPOS_CUE / CAPOS_CAPNP env values are still rejected by mkmanifest and make generated-code-check.

Schema-aware data conversion

mkmanifest cue-to-capnp converts CUE-authored data messages into arbitrary specified Cap’n Proto struct roots without routing them through the boot manifest ABI:

make cue-ensure capnp-ensure
CAPOS_CUE="$(make -s cue-path)" \
CAPOS_CAPNP="$(make -s capnp-path)" \
cargo run --manifest-path tools/mkmanifest/Cargo.toml --target "$(rustc -vV | awk '/^host:/ {print $2}')" -- \
	cue-to-capnp --import-path schema input.cue schema/example.capnp Example output.bin

The subcommand accepts the same CUE --package, --tag, and CAPOS_CUE_TAGS inputs as the manifest builder. It also accepts repeated --import-path <dir> or -I<dir> arguments plus --no-standard-import, which are passed to capnp convert as process arguments, not through a shell. The input CUE is first exported to JSON, then the pinned Cap’n Proto tool validates that JSON against the named schema and root struct.

This is the right path for configuration blobs, demo fixtures, or future schema-defined records that are not SystemManifest. It still cannot encode live capOS capability table entries or meaningful Cap’n Proto interface objects; authority transfer remains an IPC/runtime concern.

Limits and non-goals

  • A second kind: "operator" seed account is rejected by the kernel in slice 2; multi-operator support is tracked in User Identity and Policy.
  • The slice-2 overlay is not a replacement for cloud-instance configuration; cloud-metadata-driven manifest deltas are designed in Cloud Metadata.
  • The overlay does not auto-ingest ~/.ssh/*.pub; conversion is manual by design (security review on which keys count).
  • Focused-proof manifest migration onto the defaults package (slice 3, Task 2) is complete: every repo-root system-*.cue manifest declares its own CUE package and imports the defaults package, except system-paperclips.cue and system-adventure.cue (demo-owned, package-less but still importing defaults) and system-measure.cue (held by the measure-mode-repair plan). The Slice-3 inventory table in System Configuration and Operator Extensibility records the per-manifest status, package, and make run-* target.