# Configuration

The default capOS boot manifest (`system.cue` at the repo root) is layered
on a shared scaffold in [`cue/defaults/defaults.cue`](../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`](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`:

```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:

```bash
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

```cue
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:

```cue
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):

```bash
# 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"`:

```cue
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

```cue
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:

```make
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`.

## 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:

```bash
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
  [`docs/proposals/user-identity-and-policy-proposal.md`](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`](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 are migrating onto the defaults package in
  slice 3. `system-spawn.cue`, `system-shell.cue`, `system-terminal.cue`,
  `system-telnet.cue`, `system-login.cue`, `system-login-setup.cue`,
  `system-local-users.cue`, and `system-credential.cue` are already
  packaged; `system-ssh-*.cue` and the remaining variants still use their
  legacy single-file shape until their focused checks move with them.
