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 covers the slice-2 extension surface. The design rationale lives in docs/proposals/system-configuration-proposal.md.

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.

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.

Worked examples

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).

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
	pendingIpcSubmissionLimit: 8
	ringScratchLimitBytes:     16384
	logQuotaBytesPerWindow:    32768
	networkProfile:            "none"
	cpuBudgetUsPerWindow:      10000
	cpuWindowUs:               100000
	timerWaiterLimit:          2
	launcherProfile:           "bootstrap-guest"
}]

Reference the profile name from extraSeedAccounts[].resourceProfile.

Host-user injection (@tag(user))

make run runs cue export with --inject user=$(USER), so the operator session shows the host login as the displayName. Other Make targets leave the tag unset, keeping the canonical operator value that smoke harnesses assert against. 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 reads CAPOS_CUE_TAGS from the environment and forwards each key=value pair as --inject key=value. The Makefile sets it target-scoped to make run only:

run: CAPOS_CUE_TAGS = user=$(USER)

Set additional tags via CAPOS_CUE_TAGS=user=alice,region=eu-west make run or by passing --tag key=value to mkmanifest directly. system.cue only consumes user today; 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.

Limits and non-goals

  • A second kind: "operator" seed account is rejected by the kernel in slice 2; multi-operator support is tracked in docs/proposals/user-identity-and-policy-proposal.md.
  • The slice-2 overlay is not a replacement for cloud-instance configuration; cloud-metadata-driven manifest deltas are designed in docs/proposals/cloud-metadata-proposal.md.
  • The overlay does not auto-ingest ~/.ssh/*.pub; conversion is manual by design (security review on which keys count).
  • Focused-proof manifests (system-spawn.cue, system-shell.cue, system-ssh-*.cue, etc.) remain package-less in slice 2 and are evaluated in single-file mode. Slice 3 will migrate them onto the defaults package.