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 indocs/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.