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

Proposal: System Configuration and Operator Extensibility

Current operator-facing design authority now lives in Configuration. Manifest/startup authority lives in Manifest and Service Startup. This proposal is retained as the archival rationale and implementation history.

A small, layered CUE configuration model for the boot manifest that lets operators extend the default boot (system.cue) without forking it, unifies the host operator into a single principal regardless of which authentication method they use, and moves the per-user toolchain cache out of the repository root.

Problem

The default boot manifest (system.cue) and its focused-proof siblings (system-spawn.cue, system-shell.cue, the various system-ssh-*.cue, etc.) are each self-contained CUE files with a large shared scaffold copy-pasted across them. Three concrete pain points follow from that.

  • No clean operator extension surface. An operator who wants to add their own SSH public key, a second principal, or a different MOTD has to edit system.cue directly and carry that as a local diff against main. There is no documented “drop a small file, get an overlay” mechanism, so changes accumulate as untracked checkout-local state or get lost during git pull.
  • No host-user awareness. The default operator account in system.cue is hardcoded as name="operator" / displayName="operator". The host user typing make run sees a generic login identity, and adding their real SSH key requires manual conversion of the .pub file into the manifest’s hex format. The build environment already knows the host user ($USER), the SSH key (~/.ssh/id_ed25519.pub), and the typical operator preferences; none of that information reaches the manifest.
  • Superseded cache default: the original implementation used $(GIT_COMMON_DIR)/../.capos-tools, which created one pinned-tool cache per clone. The implemented default is now $(HOME)/.capos-tools through CAPOS_TOOLS_ROOT, with per-version subdirectories such as limine/<commit>/ and cue/<version>/.

Adjacent design pressure: the SSH Shell Gateway milestone needs a plausible answer to “where does the host operator’s SSH key go?” before its run-target/init-mandate Gate D can close, and the local-users backlog wants the host operator’s session to be a single account with multiple authentication bindings (password, SSH key, future passkey) rather than parallel operator/ssh-operator/passkey-operator seeds.

Design

The proposal is four small, independent moves that compose into one operator-facing extension surface.

1. Per-user toolchain cache

CAPOS_TOOLS_ROOT defaults to $(HOME)/.capos-tools instead of $(GIT_COMMON_DIR)/../.capos-tools. The override path stays available (set the variable explicitly to relocate). Existing per-version subdirectories (limine/<commit>/, cue/<version>/, etc.) keep multiple capOS clones from colliding on a single host. The first make after the change repopulates the new path; the old in-repo .capos-tools/ is left in place and can be removed manually.

Slice 2 must update every consumer that derives the pinned CUE path from the old default. At minimum:

  • tools/mkmanifest::expected_cue_path validates CAPOS_CUE against $(CAPOS_TOOLS_ROOT)/cue/<version>/bin/cue.
  • tools/check-generated-adventure-content.sh recomputes the same path in shell and is invoked by make generated-code-check. If the Makefile exports CAPOS_CUE to the new path but the script recomputes the old one, the generated-code gate will reject the pinned CUE binary.

Any future tool that pins repo-selected helpers must follow CAPOS_TOOLS_ROOT in lock step with the Makefile.

This change is independent of the rest of the proposal — it could ship on its own — but it is bundled because the same operator-extension narrative covers it: per-user state belongs in $HOME, not in the repository.

2. cue/defaults/ package, packaged-default directory, and overlay shape

A new cue/defaults/defaults.cue declares package defaults and exports #DefaultSystem capturing the shared scaffold. The manifest decoder reads root-level schemaVersion, binaries, initConfig, and kernelParams (with seed accounts, resource profiles, authorized SSH keys, MOTD, UART config, and log level all nested under kernelParams), so #DefaultSystem mirrors that exact shape — final fields are at the document root, with kernelParams holding the kernel-side config tree:

  • binaries declarations common to interactive boots
  • initConfig.init and initConfig.services skeletons for the password-login + anonymous-shell flow
  • kernelParams.consoleUart / kernelParams.terminalUart / kernelParams.logLevel
  • kernelParams.seedAccounts with a single canonical host-operator entry (32-byte fixed principalId)
  • kernelParams.resourceProfiles with a single canonical operator resource profile
  • kernelParams.motd
  • kernelParams.authorizedSshKeys (empty by default)
  • A documented set of appendable extension inputs (see below) that overlays use to extend lists. CUE list unification is element-wise conflict, not concatenation; CUE v0.16 also rejects the legacy [a] + [b] list-arithmetic form, requiring list.Concat from the standard library.

The repo’s cue.mod/module.cue declares module: "capos.local" with language v0.16.0. The defaults package lives at cue/defaults/ and uses package defaults (not package capos) so the root overlay can import it without a self-import.

The packaged default manifest stays at the repo root as system.cue, declaring package capos. The overlay companion is system.local.cue (repo root, package capos, gitignored). Focused-proof manifests migrate independently to their own packages so they can import the defaults package without joining package capos. Every repo-root system-*.cue manifest now 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 (owned by the measure-mode-repair plan and intentionally not migrated yet). See the Slice-3 inventory table below for the full mapping.

Keeping the default manifest at the repo root preserves the current embed_binaries contract — tools/mkmanifest resolves binaries[].path relative to the manifest’s parent directory and rejects .., so the manifest must live in a directory from which existing repo-root-relative paths like init/target/... are reachable. Moving the default into a subdirectory would force a parallel binary-path-base change in mkmanifest; that is not worth the additional surface for the value of co-locating the overlay.

// system.cue (repo root, packaged default)
package capos

import defaults "capos.local/cue/defaults"

_user: string | *"operator" @tag(user)

#Manifest: defaults.#DefaultSystem & {
    user: _user
}

// Final manifest fields the decoder consumes are at document root.
// The decoder ignores any unused names like #Manifest.
schemaVersion: #Manifest.schemaVersion
binaries:      #Manifest.binaries
initConfig:    #Manifest.initConfig
kernelParams:  #Manifest.kernelParams

The default MOTD value lives only in the defaults package (motd: string | *_defaultMotd, where _defaultMotd is the multi-line capOS welcome with chat/adventure shell hints — see cue/defaults/defaults.cue). system.cue does not assign MOTD itself, so a cue export .:capos without an overlay still resolves to a complete value — two sibling string | *"..." defaults from different files would unify to “incomplete” in CUE v0.16. An overlay refines the field by declaring a concrete value (no *), which is more specific than the default and wins under unification:

// system.local.cue (overlay)
package capos

#Manifest: kernelParams: motd: "Hi alice — capOS dev box."

tools/mkmanifest today invokes cue export <file> against a single file path; CUE then loads only that file (plus its imports) and does not unify other root files even when they share a package name. Slice 2 adds a --package <name> flag that switches mkmanifest to cue export <dir>:<name> (where <dir> is the file’s parent and <name> is capos). The Makefile passes --package capos only for the default-boot recipe; focused make run-* targets keep single-file mode and are not affected by the new packaged default.

Two Makefile changes are required for slice 2 to be safe:

  1. The manifest.bin rule’s prerequisites must include the defaults package (cue/defaults/*.cue) and system.local.cue (when it exists). Otherwise, edits to those files leave a stale manifest.bin and make run boots the previous configuration.
  2. Tag-dependent builds (CAPOS_CUE_USER=$(USER) and optional CAPOS_CUE_DISPLAY_NAME=...) must invalidate cached manifest.bin when the tag value changes. The intended pattern is a sentinel file under target/ whose contents record the tag values; the manifest rule depends on the sentinel, and the sentinel is regenerated whenever the CUE tag environment changes. Without this, make run after a make run-smoke (different tag) silently boots the cached operator-tagged manifest.

3. @tag(user) injection contract

The host user name is injected into the manifest at cue export time via a CUE tag. Because the manifest’s authoritative tag site must be in a file that cue export actually reads, the tag is declared in the root overlay file (system.cue), not the imported defaults package. CUE evaluates tag attributes at the file where they are declared.

The tag site is in the packaged default manifest file (system.cue at the repo root, shown above) — that file declares _user @tag(user) and threads it into defaults.#DefaultSystem via the user field. The defaults package itself does not need a @tag because tags are evaluated where they appear in the input.

// cue/defaults/defaults.cue (excerpt)
package defaults

import "list"

// Fixed 32-byte principal ID — manifest validation rejects shorter
// or longer values. Only display strings vary by host user; the
// audit-correlatable principal stays stable.
_canonicalOperatorPrincipalId: "local-operator-principal-default"

#DefaultSystem: {
    user: string | *"operator"

    schemaVersion: 1
    binaries:      [...] // shared list
    initConfig:    {...} // anonymous-shell flow

    extraSeedAccounts:      [...#SeedAccount]      | *[]
    extraResourceProfiles:  [...#ResourceProfile]  | *[]
    extraAuthorizedSshKeys: [...#AuthorizedSshKey] | *[]

    kernelParams: {
        motd:        string | *"capOS default boot. Type 'login' or 'setup'."
        consoleUart: {...}
        terminalUart: {...}
        logLevel:    string | *"debug"

        seedAccounts: list.Concat([[{
            name:            user
            displayName:     userDisplayName
            principalId:     _canonicalOperatorPrincipalId
            kind:            "operator"
            // ...
        }], extraSeedAccounts])

        resourceProfiles: list.Concat([[{
            name: "default-operator-profile"
            // ...
        }], extraResourceProfiles])

        authorizedSshKeys: extraAuthorizedSshKeys
    }
}

tools/mkmanifest today invokes cue export <path> from Rust and does not pass --inject / -t flags. Slice 2 adds a tag pass-through: either a new mkmanifest --tag user=alice CLI option that mkmanifest forwards to the underlying cue export, or — simpler — mkmanifest reads environment tags and forwards each value as --inject key=value. The Makefile sets CAPOS_CUE_USER=$(USER) for make run only; mkmanifest derives displayName from that same account’s passwd comment unless CAPOS_CUE_DISPLAY_NAME is explicitly set. make run-smoke and CI-shaped targets leave them unset, so untagged system.cue continues to see account=operator / display=operator; focused smoke manifests may pin demo-specific account fixtures independently.

@tag is the standard CUE pattern for build-time string injection and is preferred over preprocessing the file with sed or generating a wrapper file. It generalizes: future tags can carry hostname, locale, timezone, or other build-environment-derived values without adding more mechanisms.

4. system.local.cue overlay hook

The overlay file is system.local.cue at the repo root, declaring package capos. It is gitignored explicitly. CUE in package mode ignores files whose names start with ., so a leading-period variant would not be loaded; the chosen filename has no leading dot.

In package mode (slice-2 mkmanifest invocation cue export .:capos), CUE unifies every non-hidden *.cue file in the directory that declares package capos — today that is just system.cue; once the operator adds system.local.cue, both files are unified automatically with no imperative include. Focused-proof manifests are not picked up because migrated variants use their own package names and unmigrated variants remain package-less.

A checked-in system.local.cue.example (repo root, package capos) documents the supported extension shapes with worked examples. The operator copies it to system.local.cue to activate.

Appendable extension inputs

CUE list unification is element-wise conflict, not concatenation, so an overlay cannot extend the defaults’ seedAccounts or authorizedSshKeys by re-assigning the same field. The defaults package therefore exposes named extension lists that it concatenates into the final manifest fields:

See the defaults excerpt above for the appendable inputs (extraSeedAccounts, extraResourceProfiles, extraAuthorizedSshKeys) and how they are concatenated via list.Concat — the form [a] + [b] is rejected by CUE v0.16.

The overlay populates the extra* fields on #Manifest (which is the named definition produced by the packaged-default file), never the final lists:

// system.local.cue (repo root, gitignored, copied from .example)
package capos

#Manifest: extraAuthorizedSshKeys: [{
    keyId:                "host-laptop-ed25519-2026-04"
    principalId:          "local-operator-principal-default"
    algorithm:            "ssh-ed25519"
    publicKey:            "hex:..."          // see how-to doc
    fingerprintSha256:    "..."
    allowedShellProfiles: ["operator"]
    source:               "manifest"
    comment:              "host laptop"
}]

The principal id stays the fixed 32-byte canonical value — the overlay does not derive a per-user principal id. Display strings change with @tag(user); the audit-correlatable identity does not.

Worked extension scope (slices 2 and 3)

The overlay ships supporting these operator extensions:

  • MOTD: re-declare #Manifest.kernelParams.motd in the overlay with a concrete string. The default is string | *"...", so a more concrete overlay value wins under CUE unification.
  • Console password verifier: override #Manifest.kernelParams.consolePasswordVerifierPhc (Argon2id PHC string) so the development verifier shipped by the defaults package is replaced for any non-research deployment.
  • Extra SSH keys for the host operator: append to extraAuthorizedSshKeys with principalId matching the canonical operator. Multiple keys allowed.
  • Extra non-operator principals: append to extraSeedAccounts with kind: "guest", kind: "service", or future kinds. Adding a second kind: "operator" is not supported in slice 2kernel/src/cap/mod.rs::operator_seed_account rejects manifests with more than one operator seed for password login. Multi-operator support is a separate change in the user-identity-and-policy track.
  • Extra resource profiles: append to extraResourceProfiles for custom quota templates referenced by extra accounts.
  • Extra boot binaries: append to extraBinaries with name and repo-relative path. The defaults package concatenates the list onto its _baseBinaries so mkmanifest embeds the operator binary into manifest.bin alongside the default service set.
  • Extra init-launched services: append to extraServices with name, binary (resolved against binaries), restart, and the cap graph the service should receive at spawn. The defaults concatenate operator extras after _baseServices, so init starts the operator service after the default chat server, remote-session gateway, and shell.

Task 4 closeout (2026-05-03 18:51 EEST): system.local.cue.example covers every extension above. The plan calls for make run as the verification target, but make run is interactive, so verification ran make manifest (default MANIFEST_SOURCE=system.cue, package mode --package capos) with the example copied to system.local.cue. The package-mode rebuild emitted the operator MOTD into manifest.bin (3 services, 12 binaries → 2551416 bytes, log target/manifest-refreshed-example.log); rebuilding the same target with the overlay absent produced 2553224 bytes, confirming the operator MOTD overrode the defaults’ default value. make run-smoke was not a useful overlay verification because that target builds manifest-smoke.bin from system-smoke.cue in single-file mode (no --package flag, no sibling-file unification); md5 of manifest-smoke.bin was identical with and without the overlay file present.

The proposal does not generate the SSH hex/fingerprint conversion in the Makefile — that lives in docs/configuration.md as a short ssh-keygen -lf ~/.ssh/id_ed25519.pub + xxd/base64 -d pipe. Keeping this manual avoids importing arbitrary host SSH keys into the boot manifest by default.

5. Single-account-multi-auth invariant

The host operator is one account with potentially many authentication bindings:

  • Password verifier — current consoleCredential PHC blob; bound to the host operator account by being declared at the same manifest scope (today there is no explicit principalId reference in the credential record, but the kernel resolves the operator principal from the seed account at session-mint time).
  • SSH public keys — multiple records in authorizedSshKeys, each carrying principalId matching the host operator’s seed account.
  • Future passkey/OIDC bindings — same pattern; the user-identity-and-policy proposal already shows ExternalIdentityBinding shaped this way.

The kernel’s operator_session_metadata already pulls the principal from the manifest seed account when present (see kernel/src/cap/session_manager.rs OperatorSeedAccount); the hardcoded compatibility fallback fires only when no seed account is declared. Once system.cue declares the host-operator seed account explicitly, both password login and SSH public-key login mint a session for the same principal. The AuthorityBroker.shellBundle path is unchanged — it already routes through the AccountStore by principal id (after the SSH AccountStore-bound auth slice landed at commit 33100f4).

Importantly: this is not a kernel change. It is a manifest-shape choice that makes the existing kernel resolution path the canonical one. The bootstrap fallback (no seed account → hardcoded operator principal) stays in place for focused proofs that intentionally test the no-account-store path.

Migration Plan

SliceScopeRisk
1 (this)Proposal + task ledger pointer + index entry. No code.None.
2Makefile (CAPOS_TOOLS_ROOT default, CAPOS_CUE_TAGS sentinel-file dependency for make run, manifest-rule prerequisites for the defaults package and system.local.cue); cue/defaults/defaults.cue; system.cue rewrite (stays at repo root, becomes package capos); system.local.cue.example (committed at repo root); tools/mkmanifest package-mode flag (--package capos switching to cue export <dir>:capos), tag pass-through, and updated expected_cue_path for the new tools-root default; docs/configuration.md; CLAUDE.md project-layout note.Medium — touches Makefile, mkmanifest CLI surface, the default boot manifest, and adds a new package directory. Smoke harness assertions on principal=operator must keep passing because slice 2 leaves the default tag at operator.
3Migrate focused-proof variants onto the defaults package. Closed at commit a50f610d (2026-05-03 21:54 UTC): Task 2 migrated the owned set (see the Slice-3 inventory table below), Task 3 tightened the manifest decoder to reject unknown root fields with regression tests at commit f3d89757 (see the Slice-3 Task-3 closeout below), Task 4 refreshed system.local.cue.example and docs/configuration.md to cover every defaults-package extension hook, and Task 5 stamped this status header, the task ledger System Configuration ad-hoc bullet, and the docs/changelog.md entry. One commit per variant or grouped by audit area.Low per variant once slice 2 is in. Coordinated with parallel agents to avoid worktree collisions.
4Add mkmanifest cue-to-capnp, a general host-side conversion path for CUE-authored data messages rooted at a caller-specified Cap’n Proto struct. The tool reuses the slice-2 CUE package/tag machinery, validates both CAPOS_CUE and CAPOS_CAPNP against the pinned per-user tool cache, checks cue version v0.16.0 and Cap'n Proto version 1.2.0, passes import paths through safe Command arguments, and writes the converted binary only after capnp convert json:binary succeeds.Low for boot behavior because the existing manifest pipeline is unchanged. Medium host-tool risk because schema, CUE, and JSON are hostile inputs; the implementation delegates Cap’n Proto type rules to the pinned upstream converter and keeps filesystem/process boundaries explicit.

Slice-3 manifest inventory

The table below records the migration state of every repo-root system-*.cue manifest at the slice-3 Task-2 closeout. “Imports defaults” means the file declares a CUE package and pulls in capos.local/cue/defaults. “Migration shape” distinguishes between manifests that unify the full defaults.#DefaultSystem scaffold (and inherit MOTD, seed accounts, resource profiles, the base service graph, etc.) and focused-proof manifests that intentionally reference the defaults package only as a constant lookup for schemaVersion, logLevel, and UART configuration. Both shapes are valid migration targets — focused proofs need a narrow cap graph and cannot inherit the default service tree.

ManifestPackageImports defaultsMigration shapeDriven by
system.cuecaposyesfull scaffoldmake run, make remote-session-ui
system-spawn.cuespawnyesconstant lookupmake run-spawn
system-shell.cueshellyesconstant lookupmake run-shell
system-terminal.cueterminalyesconstant lookupmake run-terminal
system-credential.cuecredentialyesconstant lookupmake run-credential
system-login.cueloginyesfull scaffoldmake run-login
system-login-setup.cueloginsetupyesfull scaffoldmake run-login-setup
system-local-users.cuelocalusersyesconstant lookupmake run-local-users
system-revocable-read.cuerevocablereadyesconstant lookupmake run-revocable-read
system-memoryobject-shared.cuememoryobjectsharedyesconstant lookupmake run-memoryobject-shared
system-restricted-shell-launcher.cuerestrictedshelllauncheryesconstant lookupmake run-restricted-shell-launcher
system-chat.cuechatyesfull scaffoldmake run-chat
system-smoke.cuesmokeyesfull scaffoldmake run-smoke, make run-diagnostics, make run-iommu-acpi, make run-acpi-pcie, make run-net, make run-uefi, make run-pci-nvme, make run-ringtap-failing-call
system-session-context.cuesessioncontextyesconstant lookupmake run-session-context
system-ipc-zerocopy.cueipczerocopyyesconstant lookupmake run-ipc-zerocopy
system-service-object-routing.cueserviceobjectroutingyesconstant lookupmake run-service-object-routing
system-tcp-listen-authority.cuetcplistenauthorityyesconstant lookupmake run-tcp-listen-authority
system-capnp-chat-interop.cuecapnpchatinteropyesconstant lookupmake run-capnp-chat-interop-vm
system-thread-scale.cuethreadscaleyesconstant lookupmake run-thread-scale
system-smp-process-scale.cuesmpprocessscaleyesconstant lookupmake run-smp-process-scale
system-remote-session-capset-interop.cueremotesessioncapsetinteropyesconstant lookupmake run-remote-session-capset-interop-vm
system-remote-session-adventure-interop.cueremotesessionadventureinteropyesconstant lookupmake run-remote-session-adventure-interop-vm
system-ssh-host-key.cuesshhostkeyyesconstant lookupmake run-ssh-host-key
system-ssh-authorized-key.cuesshauthorizedkeyyesconstant lookupmake run-ssh-authorized-key
system-ssh-public-key-session.cuesshpublickeysessionyesconstant lookupmake run-ssh-public-key-session
system-ssh-public-key-auth.cuesshpublickeyauthyesconstant lookupmake run-ssh-public-key-auth
system-ssh-feature-policy.cuesshfeaturepolicyyesconstant lookupmake run-ssh-feature-policy
system-paperclips.cuenoneyesdemo-owned scaffold usemake run-paperclips
system-adventure.cuenoneyesdemo-owned scaffold usemake run-adventure
system-measure.cuenonenounmigrated; owned by measure-mode-repair planmake run-measure

system-paperclips.cue and system-adventure.cue are demo-owned and not part of the slice-3 conflict surface. They already pull #DefaultSystem for the operator account fixture but stay package-less because their make run-* targets predate the package-mode flag. Migrating them onto a package paperclips / package adventure shape is a follow-up coordinated through the demo plans rather than slice 3. system-measure.cue waits for docs/backlog/scheduler-evolution.md to close, then can be migrated in its own batch.

All manifests added after the Slice-3 closeout (C payload manifests, DDF grant manifests, hardware-audit variants, POSIX adapter smokes, WASI smokes, wasm-host, thread-fairness variants, scheduler/scheduling-context, limit proofs, and remote-session variants) follow the same convention: each declares its own CUE package and imports capos.local/cue/defaults. The table above is a Slice-3 migration snapshot; it is not exhaustive of all current repo-root system-*.cue files.

Slice-3 Task 3 closeout

Closed 2026-05-03 20:22 UTC at commit f3d89757. The SystemManifest CUE decoder (capos-config/src/manifest.rs) now validates the document root against an explicit allow-list and returns Error::UnknownField { path, field, expected } for any other top-level name. The accepted set lives in the decoder (SYSTEM_MANIFEST_ROOT_FIELDS) and is schemaVersion, binaries, initConfig, kernelParams — adding a future field is a deliberate edit to that list. Two host-side tests in capos-config/src/manifest.rs (system_manifest_rejects_unknown_root_field and system_manifest_accepts_only_known_root_fields) pin both the rejection path and the positive case so a regression is caught by cargo test-config before any QEMU run. The Cap’n Proto schema for SystemManifest is closed by construction, so the strictness check only needs to live at the CUE/JSON boundary; capnp decode paths remain unchanged. The slice-3 inventory above guarantees that every owned focused-proof manifest already projects only those four fields at the document root, so the rule does not break any migrated manifest. docs/configuration.md records the operator-facing behavior of the new error.

Slice 2 is intentionally minimal so that any breakage shows up on the default make run / make run-smoke path immediately, rather than hidden behind a fan-out of converted variants.

Slice 4 deliberately does not make CueValue universal. CueValue remains the project-defined generic tree used inside SystemManifest.initConfig. The general converter has a different contract:

mkmanifest cue-to-capnp \
  [--package capos] [--tag key=value ...] \
  [--import-path schema ...] [--no-standard-import] \
  input.cue schema/example.capnp Example output.bin

input.cue is exported as JSON, then the pinned Cap’n Proto tool validates that JSON against schema/example.capnp and root struct Example. This covers normal Cap’n Proto data fields, nested structs, lists, enums, unions, defaults, and imports according to upstream capnp convert semantics. It does not serialize live capOS capability table entries or meaningful Cap’n Proto interface objects; authority still travels through capOS capability transfer mechanics, not through JSON-authored data files.

Cross-References

  • Manifest and Service Startup — describes the CUE evaluation, boot manifest build, and general cue-to-capnp host-tool flow that this proposal extends.
  • Local Users, Storage, and Policy — Gate 1 manifest-seeded accounts; this proposal shapes the default manifest’s seed account to match the single-account-multi-auth invariant the backlog calls for.
  • Run Targets, Init Mandate, and Default-Run Integration — Gate D (default-make run integration); this proposal makes Gate D closure for the SSH milestone tractable by giving the default manifest a clean place to absorb optional services and authorized keys.
  • SSH Shell Gateway — consumes the host-user authorized-key surface in a future slice once OpenSSH transport gates land.
  • User Identity and Policy — defines the principal/account/session model and ExternalIdentityBinding shape that this proposal’s single-account-multi-auth invariant relies on. Multi-operator support is tracked there.
  • Service Architecture — primary consumer of the layered manifest: initConfig.services and the extraServices extension hook described above feed the authority-at-spawn service graph. The defaults package owns the base service tree (chat server, remote-session gateway, shell); overlays append operator-owned services without forking it.
  • Userspace Binaries — defines the binary set the layered manifest embeds. The binaries/extraBinaries shape covers native Rust capos-rt binaries, libcapos C-substrate binaries, the POSIX adapter binaries, and the wasm-host binary uniformly; per-language payload conventions (for example, the wasm-host’s stable wasi-payload manifest name) are documented there.
  • POSIX Adapter — POSIX adapter smokes (make run-posix-dns-smoke, make run-posix-pipe-smoke, make run-posix-stdio-smoke) are driven by focused system-posix-*.cue manifests that live in the same package-mode/overlay regime as the rest of the migrated manifest set. Operator-installable POSIX-ported services attach through extraBinaries/extraServices and inherit the same authority-at-spawn grants the default service tree uses.
  • WASI Host Adapter — per-instance text grants (initConfig.init.wasiArgs, initConfig.init.wasiEnv) are CUE-authored manifest fields that flow through this proposal’s package-mode evaluation; the manifest-decoder strictness invariant closed in Slice 3 Task 3 is the same gate that catches mistyped WASI argv/env field names before a payload boots.
  • System Info Capability — adjacent precedent for “rename + structural cleanup + worked Phase 2”; this proposal adopts the same status-header and cross-reference shape.
  • Trusted Build Inputs — needs entries for the new cue/defaults/defaults.cue, the system.local.cue overlay surface, the CAPOS_CUE_TAGS environment variable (and the target/-side sentinel that records it), and the host $USER value injected via @tag(user) — all become trusted boot-manifest inputs once slice 2 lands.

Non-Goals

  • This proposal does not auto-ingest ~/.ssh/id_ed25519.pub into the manifest. The system.local.cue.example shows how the operator ingests their key explicitly. Auto-ingestion is a separate decision that has security implications (which keys count? how is the hex/fingerprint conversion validated?) and should not be bundled with the configuration-shape change.
  • This proposal does not auto-start ssh-gateway in system.cue. The SSH gateway service is added when its OpenSSH transport gates close (decomposed in docs/backlog/runtime-network-shell.md). Until then, an authorized SSH key declared in system.local.cue is plumbing-only.
  • This proposal does not introduce a CUE-level imperative “include if file exists” mechanism. CUE’s same-package unification already provides the overlay behavior; the operator’s only action is to drop a file with the right package capos header.
  • This proposal does not define a remote operator-extension delivery channel (cloud-metadata, fleet config). Those are addressed by cloud-metadata-proposal.md and stay separate.

Open Questions

  • Whether principalId should ever follow the host user. This proposal fixes principalId at 32 bytes (local-operator-principal-default) so audit history is stable even if $USER changes. A future per-user-derived principal id would need a deterministic, validated 32-byte derivation and a rollover plan; that is out of scope here.
  • Where system.local.cue lives. This proposal places it at the repo root next to system.cue. That scopes the overlay to the same package capos CUE loads in package-mode export, keeps binary path resolution unchanged, and is gitignored cleanly. Focused-proof manifests are not picked up by package capos export because migrated variants use separate package names and unmigrated variants declare no package directive — so this is settled.
  • Whether to migrate focused proofs to the defaults package. Slice 3 assumes yes because it removes copy-paste, but each variant must keep its proof shape and checks. The Slice-3 inventory table above records the migration state for every repo-root system-*.cue manifest. The intentionally divergent system-measure.cue is left for a follow-up batch keyed off the measure-mode-repair plan.
  • Tag injection for run-shell / run-terminal / focused interactive proofs. Slice 2 only wires make run. If make run-shell should also personalize, slice 3 adds it; if focused proofs should always use operator, slice 3 leaves them alone.