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-*.cuemanifest declares its own CUE package and imports the defaults package, exceptsystem-paperclips.cueandsystem-adventure.cue(demo-owned, package-less but still importing defaults) andsystem-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, andmake run-*target.