# Proposal: Boot to Shell

How capOS should move from "boot runs smokes and halts" to an authenticated,
text-only interactive shell without weakening the capability model.


## Problem

The old boot path was a systems bring-up path that started fixed services,
proved kernel and userspace invariants, and exited cleanly. The completed local
console milestone added interactive login/setup and shell behavior; the later
init-owned default manifest moved that shell behind standalone `init`. The
remaining problem space is remote/web login, stronger credential policy, and
richer shell/session behavior without reintroducing ambient authority.

The first interactive milestone was deliberately modest:

- Boot QEMU or a local machine to a text console login/setup prompt.
- Start a native capability shell after local authentication or first-boot
  setup.
- Keep browser-hosted text terminal, WebAuthn/passkeys, and remote enrollment
  as later work in the same proposal family after the local console path works.
- Keep graphical shells, desktop UI, window systems, and app launchers as a
  later tier.

The risk is that "make it interactive" tends to smuggle ambient authority back
into the system. A login prompt must not become a kernel `uid`, a web terminal
must not become an unaudited remote root shell, and first-boot setup must not
be a first-remote-client-wins race.

## Scope

The completed local-console milestone covered:

- Serial/local text console login and first-boot credential setup.
- Native text shell as the post-login workload.
- Minimal `SessionManager`, `CredentialStore`, `AuthorityBroker`, and
  `AuditLog` pieces needed to launch that shell with an explicit CapSet.
- Password verifier records stored with a memory-hard password hash.
- Local recovery/setup policy for machines with no credential records.

Later in the same proposal family:

- Passkey registration and authentication for a web text shell.
- A passkey-only account path that does not require creating a password first.
- Federated login via OpenID Connect (OIDC) identity providers — device
  code on the local/serial console, authorization code + PKCE on the web
  text shell. See
  [oidc-and-oauth2-proposal.md](oidc-and-oauth2-proposal.md).

Out of scope:

- Graphical shell, desktop session, compositor, GUI app launcher, clipboard,
  or remote desktop.
- POSIX `/bin/login`, PAM, `sudo`, `su`, or Unix `uid/gid` semantics.
- Password reset by policy fiat. Recovery is a separate authenticated setup
  or operator action.
- Making authentication proofs visible to the shell, agent, logs, or ordinary
  application processes.

## Design Principles

- Authentication creates a `UserSession`; capabilities remain the authority.
- The shell is an ordinary process launched with a broker-issued CapSet.
- Console authentication, web authentication, and federated OIDC login feed
  the same session model.
- Passwords are verified against versioned password-verifier records; raw
  passwords are never stored, logged, or passed to the shell.
- Passkeys store public credential material only; private keys stay in the
  authenticator.
- OIDC ID tokens are verified against a pinned `OidcIdentityProvider`;
  the raw token never reaches the shell or audit stream as bytes.
- First-boot setup requires local setup authority or an explicitly configured
  bootstrap credential. Remote first-come setup is not acceptable.
- A missing credential store does not imply an unlocked system.
- Guest and anonymous sessions are explicit policy profiles, not fallbacks for
  missing credentials.
- Development images may have an explicit insecure profile, but that must be
  visible in the manifest and serial output.

## Architecture

The original local console boot-to-shell proof collapsed the authentication
service and interactive shell into a single userspace process. Focused
shell-led smokes still boot `capos-shell` directly as `initConfig.init` with a
narrow bootstrap CapSet (see
[service-architecture-proposal.md#process-startup-model](service-architecture-proposal.md#process-startup-model)).
The default `system.cue` path now runs `capos-shell` as an init-started service
through standalone `init`
([service-architecture-proposal.md#the-init-process-in-detail](service-architecture-proposal.md#the-init-process-in-detail)),
but the shell-side authority model is the same: it mints its own anonymous
`UserSession` and only upgrades after a password login:

```mermaid
flowchart TD
    Kernel[kernel starts one init]
    Init[standalone init or focused shell init]
    Shell[capos-shell]
    Cred[CredentialStore]
    Session[SessionManager]
    Broker[AuthorityBroker]
    Audit[AuditLog]
    Term[TerminalSession]
    Web[WebShellGateway]
    Launcher[RestrictedShellLauncher]

    Kernel --> Init
    Init --> Shell
    Shell --> Term
    Shell --> Cred
    Shell --> Session
    Shell --> Broker
    Shell --> Audit
    Session --> Broker
    Broker --> Launcher
    Cred --> Session
    Audit --> Session
    Audit --> Broker
    Web -. "future" .-> Session
```

The shell keeps the authority-holding caps needed for its session boundary
(terminal, creds, sessions, audit, broker) because the current interactive
substrate has not split login, shell, and approval into separate services. It
does not hand those caps to any child it spawns; spawn grants go through the
broker-issued `RestrictedLauncher` whose allowlist depends on the current
session's profile (empty for anonymous, full interactive shell set for
operator, and empty or narrowly policy-selected for guest). The launcher
itself is the
[service-architecture-proposal.md#spawn-mechanism](service-architecture-proposal.md#spawn-mechanism)
`ProcessSpawner` cap wrapped behind broker-enforced policy, so a shell child
cannot widen its CapSet at spawn time.
The broker returns a narrow shell bundle such as:

```text
terminal        TerminalSession
self            UserSession metadata
status          read-only SystemStatus
logs            scoped LogReader
home            scoped Namespace or temporary Namespace
launcher        RestrictedLauncher
approval        ApprovalClient
```

Early builds can omit storage-backed `home` and use a temporary namespace. They
still should not hand the shell broad `BootPackage`, `ProcessSpawner`,
`FrameAllocator`, raw device, or global service-supervisor authority by default.

## First Terminal Boundary

The first interactive console boundary should be a session-scoped
`TerminalSession`, not a widened boot `Console` cap and not a raw byte-stream
cap handed directly to login or shell processes.

`Console` stays the early-boot and panic-path output surface. The component
that owns the underlying local console transport, line discipline, edit buffer,
and later web-terminal framing can be called `ConsoleTerminal` or
`TerminalMux`; the external authority boundary is the same either way:

- only the terminal service owns raw console transport state and line buffers,
- the shell process receives the foreground `TerminalSession` cap and drives
  pre-auth password/setup input through it with per-call `echo = hidden`,
- shell children do not inherit the terminal unless the shell names it in a
  spawn plan.

A later web-shell or federated-login service that needs a separate
authentication front-end will still get its own `TerminalSession` and its
own broker-issued bundle; it does not widen authority on the local console
shell. The shell-side framing of this split — terminal-host process versus
shell process, with the terminal owning raw console state and the shell
owning the post-auth command loop — lives in
[shell-proposal.md#terminal-host-separation](shell-proposal.md#terminal-host-separation).

The first interface should stay line-oriented:

```capnp
enum LineEcho {
  visible @0;
  hidden @1;
}

enum LineStatus {
  submitted @0;
  cancelled @1;
  closed @2;
}

struct LineRequest {
  prompt @0 :Text;
  maxBytes @1 :UInt32;
  echo @2 :LineEcho;
  allowEmpty @3 :Bool;
}

interface TerminalSession {
  write @0 (data :Data) -> ();
  writeLine @1 (text :Text) -> ();
  readLine @2 (request :LineRequest) -> (status :LineStatus, line :Data);
}
```

That shape fixes the first boot-to-shell boundary:

- `readLine` returns one bounded line or a structured `cancelled`/`closed`
  result. The service owns the temporary edit buffer and scrubs it after
  completion or cancellation.
- Echo policy is per call. Password entry uses `echo = hidden`; the shell never
  toggles a terminal-global echo bit that could leak into later prompts.
- The terminal service enforces a hard implementation ceiling even if a caller
  asks for a larger `maxBytes`. `ConsoleLogin` and setup flows should request
  smaller bounds than the shell's ordinary command reader.
- Cancellation is line-scoped. Operator abort input returns `cancelled` and the
  caller receives no partial secret buffer.
- The first milestone does not need raw byte reads, terminal history replay,
  multi-reader fan-out, or shell-visible secret-state. Paste framing, resize,
  and richer terminal controls can extend `TerminalSession` later.

This keeps password/setup entry inside `ConsoleLogin` and terminal services.
The broker, audit log, shell, and shell children only see the outcomes they
need: session metadata, policy results, and a terminal handle for post-auth
interactive work.

## Console Login

The local console path now runs entirely inside `capos-shell`, so "login"
is a shell command rather than a separate pre-shell process. The shell always
boots with an anonymous session; authentication is an explicit user action.
The three states below describe what the `login` and `setup` commands see,
not a boot-time mode selector. The in-shell command surface and the
`login` / `setup` / `caps` / `inspect` command behavior live in
[shell-proposal.md#approval-and-authentication](shell-proposal.md#approval-and-authentication)
and [shell-proposal.md#interactive-command-surfaces](shell-proposal.md#interactive-command-surfaces);
this proposal describes only the session/credential/broker authority side of
the same flow. The `make run-login` smoke covers the password path, and
`make run-shell` covers the anonymous-only path.

### Password Configured

If `CredentialStore` has an enabled console password verifier for the selected
principal or profile, `login` prompts for the password, verifies it through
`CredentialStore`, mints an operator `UserSession` via `SessionManager.login`,
asks the broker for the `operator` shell bundle, and swaps the in-shell
session and launcher in place.

The verifier record should be versioned:

```text
PasswordVerifier {
  algorithm: "argon2id"
  params: { memoryKiB, iterations, parallelism, outputLen }
  salt: random bytes
  hash: verifier bytes
  createdAtMs
  credentialId
  principalId
}
```

Argon2id is the default target because it is memory-hard and widely reviewed.
The record must include parameters so stronger settings can be introduced
without invalidating older records. A deployment may add a TPM- or
secret-store-backed pepper later, but the design must not depend on a pepper
being present.

On failed attempts, the shell records an audit event and applies bounded
backoff before re-prompting within the same `login` invocation. The backoff
state is not a security boundary by itself, because local attackers may
reboot; the password hash strength still matters.

### No Console Password

If no console password verifier exists, `login` reports that setup is
required. The user must run `setup` to create the first verifier. The
`make run-login-setup` smoke drives the first-boot path: no verifier exists,
`login` refuses, `setup` mints the first volatile verifier through the
manifest operator seed principal, and the shell then upgrades to an operator
session.

Setup mode can:

- create the first console password verifier,
- enroll a first passkey for the web text shell (future),
- create both credentials (future).

Until a credential is created, the shell stays in the anonymous session: it
can exercise `caps`, `inspect`, `session`, and `help`, but the broker-issued
anonymous launcher has an empty allowlist, so the shell cannot spawn children
or escalate authority. This matches the operator expectation: no configured
password means "setup required", not "open console".

### Passkey-Only Deployment

Passkey-only should be possible without creating a password. It still needs a
bootstrap authority path.

Acceptable first-passkey bootstrap paths:

- local console setup enrolls the first passkey and then never creates a
  password verifier,
- the manifest or cloud metadata includes a predeclared passkey public
  credential for an operator principal,
- the console prints a short-lived setup challenge that a web enrollment flow
  must redeem before registering the first passkey.

Unacceptable path:

- the first remote browser to reach the web endpoint becomes administrator
  because no password exists.

If a machine is passkey-only, the local console can still expose setup,
recovery, guest, or diagnostic profiles according to policy. It should not
silently become an unauthenticated administrator shell.

## Guest and Anonymous Profiles

The user-identity proposal distinguishes authenticated, guest, anonymous, and
pseudonymous sessions (see
[user-identity-and-policy-proposal.md#anonymous-guest-and-pseudonymous-access](user-identity-and-policy-proposal.md#anonymous-guest-and-pseudonymous-access)
for the full taxonomy and
[user-identity-and-policy-proposal.md#session](user-identity-and-policy-proposal.md#session)
for the underlying session structure). Boot-to-shell should consume that
model directly.

Authenticated password login creates a human or operator `UserSession` with
auth strength `password`. Authenticated passkey login normally creates a human,
operator, or pseudonymous `UserSession` with auth strength `hardwareKey`.
Neither proof is authority by itself; both feed the broker.
Default password-authenticated local operator sessions do not expire by fixed
wall-clock timestamp; their normal lifecycle is explicit logout,
terminal/connection/process-tree close, or administrator revocation. A
manifest can still opt into a hard operator lifetime for focused proofs or
deployment policy.

Guest is the only unauthenticated profile that belongs on the local interactive
console by default. It is a deliberate `SessionManager.guest()` path with a
local interactive affordance, weak or no authentication, short expiry, tight
quotas, no durable home unless policy grants one, and a bundle such as:

```text
terminal        TerminalSession
self            guest UserSession metadata
tmp             temporary Namespace
launcher        RestrictedLauncher(allowed = ["help", "settings"])
logs            scoped LogReader for this guest session
```

Guest should not receive `ApprovalClient` for administrative actions unless a
named policy grants it. If no console password exists, setup may offer a guest
session only when the manifest explicitly enables a guest profile. Otherwise
the operator must create a credential or leave the ordinary shell unavailable.

Anonymous is different. It is usually remote or programmatic, has a random
ephemeral principal ID, receives a smaller cap bundle than guest, and has no
elevation path except "authenticate" or "create account". It is not the
console fallback for missing credentials, and it should not be counted as
"booted to shell" unless the product goal is an explicitly anonymous demo.

If the web gateway later supports anonymous access, it should be a
purpose-scoped workload or very restricted text terminal with no durable home,
strict quotas, short expiry, and audit keyed by network context plus ephemeral
session ID. It must not share the passkey setup path, because passkey-only
bootstrap is a credential-enrollment flow, not anonymous access.

An empty CapSet remains the "Unprivileged Stranger" case. It is useful for
attack-surface demonstration, but it is not a session profile and not a shell
login mode.

## Web Text Shell and Passkeys

This is later work in the same proposal family, not part of the current
local-console acceptance gate. The web shell is a browser-hosted terminal
transport, not a graphical shell. It should display the same native text shell
protocol through a terminal UI and should launch the same kind of session
bundle as the local console path.

Required pieces:

- network stack and HTTP/WebSocket or equivalent streaming transport,
- TLS or a deployment mode acceptable to browsers for WebAuthn,
- stable relying-party ID and origin policy,
- random challenge generation,
- passkey credential storage,
- user-verification policy,
- audit and rate limiting.

Passkey credential records should store public material:

```text
PasskeyCredential {
  credentialId
  principalId
  publicKey
  relyingPartyId
  userHandle
  signCount
  transports
  userVerificationRequired
  createdAtMs
}
```

The authentication flow is:

1. Browser requests a login challenge.
2. `WebShellGateway` asks `SessionManager` or `CredentialStore` for a bounded,
   random challenge tied to the relying-party ID and intended principal.
3. Browser calls the platform authenticator.
4. Gateway verifies the WebAuthn assertion, origin, challenge, credential ID,
   public-key signature, user-presence/user-verification flags, and sign-count
   behavior.
5. `SessionManager` mints a `UserSession` with auth strength `hardwareKey`.
6. `AuthorityBroker` returns the shell bundle for that session/profile.
7. `RestrictedShellLauncher` starts the native text shell connected to the
   web terminal stream.

Registration requires an existing authenticated session, local setup authority,
or an explicit bootstrap path. Passwordless registration is allowed; unauthenticated
remote registration is not.

## Remote Session Clients

The same authentication and broker model also serves non-shell remote clients.
A host app -- CLI, native GUI, Tauri backend, webapp gateway, or service
client -- should not have to start a terminal shell just to call typed
services. After password, public-key, OIDC, passkey, mTLS, guest, anonymous,
or service/workload admission succeeds under policy,
`SessionManager` mints a `UserSession` and `AuthorityBroker` returns a
remote-client bundle. The client then sees a remote CapSet view whose entries
are Cap'n Proto RPC object references, not local capOS cap slots.

This keeps boot/login policy unified:

- authentication proofs are consumed by trusted session/admission services;
- the broker chooses the CapSet for the selected profile;
- shells, web terminals, agents, and non-shell remote clients are different
  consumers of session bundles;
- password auth is one adapter, not the remote protocol shape.

The detailed remote-client design lives in
[remote-session-capset-client-proposal.md](remote-session-capset-client-proposal.md).

## Federated Login (OIDC)

OIDC is the third authentication path alongside password and passkey. It
lets capOS accept identity from a corporate IdP (Azure AD, Google
Workspace, Okta, Keycloak, Dex, GitHub) without capOS storing or
managing primary user credentials. The schemas, grant types, JWKS
handling, and token lifecycle live in
[oidc-and-oauth2-proposal.md](oidc-and-oauth2-proposal.md); this
section describes only the integration surface.

### Console (device code)

Serial consoles have no browser. The login path is RFC 8628 device
authorization:

1. `ConsoleLogin` calls `OAuthClient.startDeviceCode` on an IdP that
   the manifest has configured as acceptable for console login.
2. `TerminalSession.writeLine` prints the verification URL and user
   code; the user completes the flow on a separate device.
3. `ConsoleLogin` polls `pollDeviceCode` at the advertised
   `interval`, honoring `slow_down`. Expiry is a hard fail.
4. On `granted`, `ConsoleLogin` passes the resulting `IdToken` cap to
   `SessionManager.login(method = "oidc", proof = idTokenRef)`.
5. `SessionManager` calls `OidcIdentityProvider.verifyIdToken` with
   the client's `IdTokenPolicy`, receives `IdTokenClaims`, derives
   `PrincipalInfo.id = hash(iss, sub)`, derives `authStrength` from
   `acr`/`amr`, and mints a `UserSession`.
6. The broker returns the same shell bundle as for other login
   methods; no OIDC-specific authority flows into the shell.

Failed verification uses the same generic failure text and bounded
backoff as password login. The manifest controls which IdPs the
console accepts and which subject patterns are allowed to log in;
unlike password/passkey paths, OIDC login does not implicitly treat
"any valid token from any configured IdP" as authority — a permitted-
subject allow-list is required.

### Web text shell (authorization code + PKCE)

`WebShellGateway` offers OIDC alongside WebAuthn. The gateway drives
`OAuthClient.startAuthCode`, redirects the browser to the IdP, and
consumes the returned code through `completeAuthCode`. PKCE is
mandatory; `state` and `nonce` are generated from `EntropySource`.
The gateway validates redirect URI exactly, requires TLS, and enforces
`IdTokenPolicy.nonceMustMatch`.

### Identity provider trust

`CredentialStore` gains IdP trust records alongside password
verifiers and passkey public credentials:

```text
IdpTrustRecord {
  recordId
  issuer                 # canonical URL
  clientRegistrations    # allowed OAuthClient records for this IdP
  jwks                   # snapshot or discovery URL + pinned TLS roots
  allowedAlgorithms
  allowedAcr / allowedAmr
  subjectAllowList       # e.g. principals matching sub/email/groups
  clockSkewSeconds
  authStrengthMap        # acr/amr -> AuthStrength (X.1254 LoA)
  createdAtMs
}
```

Records are public material (IdP URLs, JWKS, policy). Like passkey
records, they can be bootstrapped from the manifest or cloud
metadata, with a bounded RAM overlay for admin-managed records until
durable storage exists. `CredentialStore.verify` stays a secret-
preserving boundary; OIDC verification that rejects a token returns
only `denied` with a generic failure class.

### Federated principal bootstrap

For a fresh image with no local password, OIDC login can create the
first `UserSession` when the manifest explicitly predeclares:

- one or more trusted issuers,
- a subject allow-list or group/claim predicate,
- the principal identities those subjects map to.

This is the OIDC analog of the manifest-declared passkey bootstrap
path: the authority comes from the manifest trust root, not from
"the first caller who presents a token wins." Without predeclared
trust, OIDC login cannot be the only path to an administrative
session on a fresh image — setup mode applies.

### Scope of tokens

Access tokens issued alongside the ID token belong to the OAuth
service. Neither the shell nor the broker ever receives raw token
bytes. If the broker needs to delegate outbound authority to the
session (e.g. "read from our corporate storage API"), it returns a
wrapper cap holding an `AccessToken` cap, not a bearer string.

### Refresh and session duration

`SessionManager` holds the `RefreshToken` cap associated with a
federated session when the IdP issues one and the scope includes
`offline_access` (or the IdP's equivalent). Token refresh is a
privileged operation scoped to `SessionManager` and audited; the
shell cannot refresh its own session token. On logout or session
expiry, `SessionManager` releases the refresh token and optionally
calls the IdP's revocation endpoint.

## Required Interfaces

These are ordinary capabilities, not kernel modes.

### EntropySource

Owns the only approved path for fresh auth/session secrets in the first
implementation.

Responsibilities:

- provide unpredictable bytes for password salts, session IDs, setup tokens,
  and later WebAuthn challenges,
- fail closed when secure randomness is unavailable instead of returning
  predictable bytes,
- keep raw entropy authority out of shells and ordinary workloads.

Only `CredentialStore`, `SessionManager`, later `WebShellGateway`, and a
future `SshGateway` or narrower SSH transport-crypto service should hold it.
`ConsoleLogin`, the shell, and spawned workloads should never mint their own
session IDs, salts, setup tokens, SSH key-exchange material, or challenges.

### CredentialStore

Owns credential verifier records and challenge state.

Responsibilities:

- list whether setup is required without exposing hashes,
- create password verifier records from setup authority,
- verify password attempts without returning the password or verifier bytes,
- register passkey public credentials,
- store trusted OIDC identity-provider records (issuer, JWKS or
  pinned discovery URL, allowed audiences, subject allow-list,
  `acr`/`amr` → `AuthStrength` mapping) so `SessionManager` can
  consume `OidcIdentityProvider` caps bound to deployment policy,
- issue and consume bounded WebAuthn challenges,
- rotate or disable credentials through an authenticated admin path.
- load bootstrap verifier/public-credential and IdP-trust records from
  manifest or cloud bootstrap config and maintain a bounded RAM overlay
  until durable storage exists.

### SessionManager

Creates `UserSession` metadata after successful authentication, explicit local
guest policy, purpose-scoped anonymous policy, or setup policy. It should
record auth method, auth strength, freshness, expiry, profile, and audit
context. It should not hand out broad system caps directly. Boot-to-shell uses
authenticated sessions and optional local guest sessions for ordinary
interactive shells; anonymous sessions are narrower remote/programmatic
contexts unless a manifest explicitly defines an anonymous demo terminal.
Session IDs come from `EntropySource`; if fresh randomness is unavailable,
authenticated login and token-bearing setup flows fail closed instead of
reusing predictable IDs. The end-to-end mint/promote sequence and the
account-store boundary it consumes are
[user-identity-and-policy-proposal.md#session-startup-flow](user-identity-and-policy-proposal.md#session-startup-flow);
the shell-side immutable-per-process invocation context that consumes the
minted session lives in
[shell-proposal.md#session-context](shell-proposal.md#session-context) and is
proven by `make run-session-context`. The `make run-local-users` smoke covers
the manifest-seeded local operator path that backs the password-login flow.

### AuthorityBroker

Maps a session/profile to a narrow CapSet. Early policy can be static and
manifest-backed. The important constraint is that the broker returns capabilities,
not roles or strings that downstream services treat as authority.

### ConsoleLogin

Consumes `TerminalSession`, `CredentialStore`, `SessionManager`, broker access,
and a restricted shell launcher. It never receives broad boot-package or device
authority unless a recovery profile explicitly grants it. It owns pre-auth
password/setup entry and must not forward raw password bytes, setup tokens, or
partial secret input into the shell, broker, or audit service.

On the current local-console substrate `ConsoleLogin` is not a separate
process. Its responsibilities are folded into `capos-shell`, which owns the
pre-auth `TerminalSession`, drives password/setup prompts, invokes
`CredentialStore`/`SessionManager`/broker, and promotes its own session
in place. The authority rules above still apply: the same process must not
leak password bytes, setup tokens, or broker secrets into spawned children.
A future web-shell or federated-login front-end can re-introduce a separate
`ConsoleLogin`-shaped service that mints sessions for a distinct shell
process.

### WebShellGateway

Terminates the browser terminal session, handles passkey challenge/response,
drives the OAuth authorization code + PKCE flow for federated login, and
connects the authenticated session to the shell process. It should not own
general administrative caps. It should ask the broker for the same narrow shell
bundle as any other session.

### SshGateway

Terminates SSH transport for CLI remote shell access, verifies host/user key
protocol state, maps accepted SSH public keys to sessions, and connects the
authenticated session to the shell process through an SSH-backed
`TerminalSession`. It should not own general administrative caps, raw
`KeyVault` administration, port-forward authority, or broad process-spawn
authority. It should ask the broker for the same narrow shell bundle as any
other session. The detailed transport and key-custody model is in
[ssh-shell-proposal.md](ssh-shell-proposal.md). The initial schema names the
supporting authority surfaces `TcpListenAuthority`, `SshHostKey`,
`AuthorizedKeyStore`, `SshTerminalFactory`, and `RestrictedShellLauncher`; the
development host-key path now exists only as an explicitly labeled
non-production QEMU proof. Bounded QEMU proofs now cover configured
authorized-key lookup, fixture public-key session minting, restricted shell
launch, and a plain-TCP terminal-host handoff, while real SSH signing,
encrypted transport, packet/channel handling, and the final OpenSSH harness
remain later gates.

### OAuthClient and OidcIdentityProvider

Supplied by the OAuth service
([oidc-and-oauth2-proposal.md](oidc-and-oauth2-proposal.md)).
`ConsoleLogin` holds an `OAuthClient` cap configured for device-code
grants against the manifest-declared IdPs, and an
`OidcIdentityProvider` cap for ID-token verification. `WebShellGateway`
holds analogous caps configured for authorization code + PKCE.
Neither service retains access tokens in long-lived session state —
refresh tokens live inside `SessionManager`, bound to the
`UserSession` lifecycle.

### AuditLog

Records setup entry, credential creation, failed attempts, successful session
creation, broker decisions, shell launch, credential disablement, and logout.
Audit entries must not include passwords, password hashes, passkey private
material, bearer tokens, complete environment dumps, or full terminal lines.
Correlate auth/session events with opaque record IDs and policy/result codes,
not with secret-bearing payloads.

## First Security Substrate

Before local setup/login code lands, the first implementation should fix these
rules:

- **Entropy source:** `CredentialStore` and `SessionManager` receive an
  `EntropySource` cap. Password salts, session IDs, setup tokens, and later
  passkey challenges come only from it. If secure randomness is unavailable,
  credential creation, authenticated session creation, setup-token issuance,
  and passkey enrollment fail closed. The only remaining boot path is an
  explicit manifest-gated guest or development profile.
- **Credential backing:** `CredentialStore` is initialized from manifest or
  cloud-bootstrap verifier/public-credential records plus a bounded RAM overlay
  for setup-created credentials and disable/rotate state. Until a real storage
  service exists, any setup-created credential and any disable/rotate action
  recorded only in that overlay is volatile and both the console UX and audit
  records must say so. The manifest may carry verifier or public-credential
  material, not raw passwords or reusable setup tokens.
- **Bounded setup-token/challenge state:** `CredentialStore` owns one bounded
  table for setup tokens and later WebAuthn challenges. Each record is bound to
  a purpose, principal/profile, opaque record ID, secret bytes, created/expiry
  times, and consumed bit. The first redemption attempt consumes the record
  whether the attempt succeeds or fails, so replay always fails closed and
  retry requires a newly minted token or challenge. Records are scrubbed on
  consume or expiry.
- **Auth failure policy:** `CredentialStore.verify` returns only `success`,
  `denied`, or `unavailable`. `ConsoleLogin` prints generic failure text and
  enforces bounded backoff without revealing whether a principal exists, which
  field mismatched, or whether a verifier came from bootstrap config or the
  RAM overlay. Permanent lockout is out of scope for the first milestone;
  bounded delay plus audit is required.
- **Audit and redaction:** `AuditLog` records structured auth/session events
  with result codes, profile, auth method, reason classes, and opaque
  credential/token record IDs. Principal/session IDs appear only after
  successful authentication or when referring to an already minted session; a
  failed pre-auth attempt logs only a terminal-local event ID plus generic
  failure class. It must never log raw passwords, verifier bytes, salts,
  setup-token/challenge secrets, passkey private material, or full terminal
  lines. When setup creates a volatile credential or RAM-only disable state,
  the audit event records `volatile = true` rather than any secret-bearing
  payload.

## Prerequisites

Boot-to-shell should not be selected before these pieces are credible:

- Default boot uses init-owned manifest execution; the kernel starts only
  `init` with fixed bootstrap authority.
- `init` can start long-lived services and not just short smoke binaries.
- `ProcessSpawner` can launch the shell and login services with exact grants.
- A `TerminalSession` path exists. Current `Console` stays output-oriented;
  login and shell work should use bounded line input with per-call echo mode
  and structured cancellation instead of raw console reads.
- The native text shell exists as a `capos-rt` binary with `caps`, `inspect`,
  `call`, `spawn`, `wait`, `release`, and basic error display.
- `EntropySource` exists for salts, session IDs, setup tokens, and later
  WebAuthn challenges, and auth/setup flows fail closed if it is unavailable.
- There is at least bootstrap verifier/public-credential backing plus a
  bounded RAM overlay. Durable credential storage can come later, but the
  first implementation must be honest about whether created credentials survive
  reboot.
- Minimal `SessionManager`, `AuthorityBroker`, and `AuditLog` services exist.
- A restricted launcher or broker wrapper prevents the shell from receiving
  broad init authority.
- Web text shell requires networking, HTTP/WebSocket or equivalent, TLS/origin
  handling, and WebAuthn verification. It can lag local console boot-to-shell.
  TLS configuration, server certificates, ACME issuance, OCSP stapling, and
  CT policy are defined in
  [certificates-and-tls-proposal.md](certificates-and-tls-proposal.md);
  WebAuthn attestation certificate verification uses the `CertVerifier` from
  that proposal against a FIDO MDS trust store.
- Federated OIDC login requires outbound TLS to the IdP discovery and
  JWKS endpoints, an OAuth client service, and manifest-declared IdP
  trust records. It depends on networking and the interfaces in
  [oidc-and-oauth2-proposal.md](oidc-and-oauth2-proposal.md). Device
  code can land with the local console path once networking exists;
  authorization code + PKCE lands with the web text shell.

## Completed Local Milestone Definition

The local-console boot-to-shell milestone completed when:

- `make run-shell` or the default boot path reaches a text login/setup prompt.
  The focused proofs are `make run-terminal` for the bounded line-discipline
  `TerminalSession` surface, `make run-credential` for the password-verifier
  store, `make run-login` for the password-login path, `make run-login-setup`
  for the first-boot setup path, `make run-local-users` for the
  manifest-seeded local-operator path, and `make run-shell` for the
  anonymous-only path.
- With a configured password verifier, the console refuses the shell on a bad
  password and launches it on the correct password.
- With no console password verifier, the console enters setup mode and requires
  creating a credential or selecting an explicitly configured local guest or
  development policy before launching a normal shell.
- If secure randomness is unavailable, setup and authenticated login fail
  closed; only explicitly enabled guest or development profiles may continue.
- Guest console sessions, when enabled, are created through
  `SessionManager.guest()` and receive only terminal/tmp/restricted-launcher
  style caps with no administrative approval path by default.
- Anonymous sessions are not used as the missing-password console fallback and
  are not accepted as proof that the ordinary boot-to-shell milestone works.
- The shell starts with a broker-issued CapSet and can prove at least one typed
  capability call plus one exact-grant child spawn through a granted launcher
  or other explicitly scoped spawn authority.
- `ConsoleLogin` drops its `TerminalSession` once the shell starts, and a
  shell-spawned child without an explicit terminal grant cannot use the
  terminal.
- Audit output records setup/auth/session/broker/shell-launch events without
  leaking secrets.
- Web text shell, passkey-only enrollment, and remote setup remain later work
  in this proposal family after the local console path exists.
- Graphical shell work is not part of the acceptance criteria.

## Implementation Plan

1. **Text console substrate.**
   `TerminalSession` is the first interactive console boundary. Keep
   `Console` output-only; terminal services own bounded line buffers, per-call
   echo mode, and cancellation behavior.

2. **Native shell binary.**
   The shell proposal's minimal REPL over `capos-rt` lists CapSet entries,
   inspect metadata, call granted capabilities including `TerminalSession`, use
   a granted restricted launcher or other scoped spawn authority for
   exact-grant child launch, wait, release, and print typed errors. The
   ordinary shell profile must not depend on `BootPackage` or broad
   `ProcessSpawner` authority.

3. **Credential store prototype.**
   Manifest/cloud-bootstrap-backed verifier and public-credential records,
   a bounded RAM overlay for setup-created credentials, `EntropySource`
   integration for salts/session IDs/tokens, and Argon2id verification anchor
   the local path. Host-generated verifier inputs are bootstrap configuration,
   not acceptance evidence for future credential work.

4. **Console setup/login.**
   The configured-password path and no-password setup path are implemented.
   Setup creates verifier state through `CredentialStore`, not ad hoc shell
   process config. The local password path now prompts for `username>` before
   hidden `password>`, routes `SessionManager.login` through an
   account/principal selector plus proof/source metadata, verifies only
   selected accounts that own `console-password`, and migrates the existing
   seeded console password to an explicit default `operator` account without
   creating username-enumeration terminal differences. Durable account-local
   verifier records remain future storage-backed work.

5. **Minimal session and broker.**
   `UserSession` metadata and the policy broker return a narrow shell bundle.
   Anonymous bundles stay separate from ordinary shell login, and QEMU proofs
   show the shell cannot obtain broad boot authority by default.

6. **Audit and failure policy.**
   Generic auth failure handling, bounded attempt backoff, hidden password
   entry, and redacted audit records are part of the completed local path.
   Future passkey/setup-token challenge state must preserve the same no-secret
   logging rule.

7. **Web text shell gateway.**
   After networking and a terminal transport exist, add WebAuthn registration
   and authentication for the browser-hosted terminal. Support passkey-only
   enrollment through local setup or explicit bootstrap authority.

8. **Federated OIDC login.**
   Add `OAuthClient`/`OidcIdentityProvider` integration to
   `ConsoleLogin` (device code) and `WebShellGateway` (auth code +
   PKCE). Extend `CredentialStore` with IdP trust records. Map
   `acr`/`amr` claims to `AuthStrength`. Require a manifest-declared
   subject allow-list for administrative sessions.

9. **Durability and recovery.**
   Move credential and IdP-trust records from boot config or RAM into a
   storage-backed service once storage exists. Define recovery as a
   credential-admin operation, not an implicit bypass.

## Security Notes

- Password hashing belongs in userspace auth services, not the kernel fast path.
- WebAuthn challenge state must be single-use and bounded by expiry.
- The web gateway must validate origin and relying-party ID; otherwise passkey
  authentication is meaningless.
- Setup tokens are credentials. They must be short-lived, single-use, audited,
  and hidden from ordinary process output.
- Credential records are sensitive even though they are not raw secrets; avoid
  printing them in debug logs.
- The shell and any agent running inside it must treat logs, terminal input,
  files, web pages, and service output as untrusted data.

## Non-Goals

- No graphical shell in this milestone.
- No passwordless remote first-use takeover.
- No kernel `uid`, `gid`, `root`, or login mode.
- No default shell access to broad `BootPackage`, raw `ProcessSpawner`,
  `DeviceManager`, raw storage, or global supervisor caps.
- No authentication proof passed through command-line arguments, environment
  variables, shell variables, audit records, or agent prompts.

## Open Questions

- Which Argon2id parameters fit the early userspace memory budget while still
  resisting offline guessing?
- How should durable storage merge bootstrap verifier records with the first
  RAM overlay once a storage-backed credential service exists?
- How should local console setup prove physical presence on cloud VMs where
  serial console access may itself be remote?
- What is the first acceptable TLS/origin story for QEMU and local development
  WebAuthn testing?
- Should passkey-only machines keep a disabled console password slot for later
  recovery, or should recovery be entirely credential-admin/passkey based?
