# Atrium

> Technical Reference Manual — the Peios web session host: the three-process architecture, logon and session jobs, the shell's state mirror, and the application contract.

---

# 1.1 Overview

_Peios / Advanced Peios / Atrium / Introduction_

> Atrium is the Peios web UI — a browser-delivered interactive session. The decisions that would not be guessed from the name.

**Atrium** is the Peios web UI. A browser pointed at a Peios machine
gets a login page; a successful logon gets a desktop — windows, apps, a
terminal — served by that machine and running on it. Atrium is not a
management panel over the system; the browser tab *is* the user's
session.

Four decisions shape everything else in this manual:

- **The browser is a renderer.** All session state — which windows
  exist, which is focused, the chord table — lives on the machine, in
  the user's session host. Every connected browser tab holds a mirror
  of that state and asks for changes; no tab owns anything. Two tabs,
  or a laptop and a phone, are two views of one desktop.
- **The shell is thin; applications are the product.** The shell owns
  login, windows, focus, keybinds and the launcher, and nothing else.
  Everything a user opens is an application: a directory of web files
  with a manifest, drawn in an iframe, talking through one SDK
  (chapter 5).
- **Privilege is split across three processes.** The one privileged
  process holds no network socket and parses no HTTP; the process that
  faces the network runs with every privilege deleted; the process
  that does the user's work is the user (chapter 2).
- **Atrium does not create user processes.** Session hosts are
  submitted to peinit as jobs, with the logon token as the job
  identity — peinit is their parent, supervisor and record-keeper
  (§3.2).

Atrium listens on port 8080, unencrypted, and every browser-facing
door is one HTTP origin. Transport security and per-application
origins are not provided in this version.

---

# 1.2 Terminology

_Peios / Advanced Peios / Atrium / Introduction_

> The three session concepts — browser, interactive, logon — and the smaller vocabulary of shell, mirror, window and application.

Three different things are reasonably called "a session", and Atrium
keeps them distinct:

| Term | What it is | Owned by |
|---|---|---|
| **Logon session** | The kernel's unit of identity: created by authd per successful logon conversation, identified by the token's LUID, spanning every process carrying that token | KACS / authd |
| **Interactive session** | One running desktop: the `atrium-session` process and its state — windows, focus, applications, terminals | Atrium |
| **Browser session** | One browser's binding to an interactive session: a cookie | atrium-server |

They are one-to-one-to-one today — one login produces one logon
session, one interactive session and one cookie — but the concepts
diverge as features arrive: a second device attaching to an existing
desktop adds a browser session without a logon; an unlock re-runs a
logon conversation without touching the interactive session.

The rest of the vocabulary:

- **Shell** — the chrome a logged-in browser renders: topbar, sidebar,
  Toolbox, window frames. One page, served by the session host.
- **Mirror** — one connected shell's copy of the session state, kept
  current over a websocket (§4.1).
- **Window** — one open application instance: an entry in the session's
  window list, and one iframe in every mirror.
- **Application (app)** — an installed directory under
  `/usr/share/atrium/apps/<id>/` with a manifest; what the Toolbox
  launches (chapter 5).
- **Session host** — the `atrium-session` process of one interactive
  session.
- **The bus** — the postMessage path between an application's frame,
  the shell, and the session host (§5.4).

---

# 2.1 The Three Processes

_Peios / Advanced Peios / Atrium / Architecture_

> atriumd, atrium-server and atrium-session — who runs as what, who spawns whom, and why the privilege sits where it does.

Atrium is three programs, split so that privilege, network exposure and
user code never share a process.

| Process | Identity | Started by | Role |
|---|---|---|---|
| `atriumd` | SYSTEM | peinit (service `atriumd`) | Originates logons with authd, holds the session table, spawns the other two |
| `atrium-server` | SYSTEM with every privilege deleted | atriumd (fork) | The only network-facing process: TCP :8080, TLS-less HTTP, cookies, forwarding |
| `atrium-session` | The logged-on user | peinit (submitted job) | One per interactive session: serves the shell, holds session state, runs applications |

**atriumd** is deliberately the smallest of the three. It runs as
SYSTEM because originating a logon takes that today — `/run/logon.sock`
admits SYSTEM alone — and everything it does follows from holding that
position: it conducts the PGSS Logon conversation, keeps logon tokens
in its session table, submits session hosts to peinit, and relays the
login exchange for the server. It parses no HTTP and never installs a
token on anyone. If a feature appears to belong in atriumd, it belongs
in one of the other two.

**atrium-server** is forked by atriumd at startup with a copy of
atriumd's own token from which every privilege has been deleted
(`Token::restrict`): the same user SID, so nothing about the
filesystem changes, but nothing a compromised HTTP parser can spend.
It asks the kernel for SIGTERM on parent death, so it cannot outlive
atriumd. It holds exactly the routing view — cookie → session — and
never sees a token.

**atrium-session** is not a child of Atrium at all. On a successful
logon, atriumd submits it to peinit's jobs channel (PSPU §7) with the
freshly minted logon token attached as the job identity — so peinit,
the one process on the machine that installs primary tokens for
others, is its parent, and the job record, cgroup and output capture
come with that. The session host starts with no privilege and needs
none: it *is* the user.

The processes an application starts — an `exec` command, a terminal's
shell — are children of the session host, inside the user's logon
session and the job's cgroup.

---

# 2.2 Sockets and Descriptors

_Peios / Advanced Peios / Atrium / Architecture_

> One socketpair per relationship, descriptors handed rather than dialed, and what the server rewrites on the way through.

Nothing in Atrium dials a rendezvous socket. Every internal link is a
socketpair created by the process that owns the relationship, with one
end handed down at spawn — trust is structural, and there is no path
to clean up and no peer to verify.

- **atriumd ↔ atrium-server**: one socketpair, created before the
  fork; the child finds its end on descriptor 3. It carries the control
  protocol — the login conversation relay, and the handover of each
  new session's socket. Frames are a 32-bit little-endian length
  prefix followed by one JSON document; a file descriptor, when one
  travels, rides as `SCM_RIGHTS` on the length prefix, so a frame and
  its descriptor cannot separate.
- **atriumd → atrium-session**: the session's control socket, created
  by atriumd and attached to the peinit job submission as the
  descriptor named `atrium-control`; peinit places it on descriptor 3
  of the session host. The server holds the other end.
- **atrium-server → atrium-session, per connection**: for each browser
  connection bound to a session, the server creates a fresh
  socketpair, sends one end down the session's control socket, and
  pumps the connection's bytes through the other for the connection's
  life. To a session host, "accept a connection" means "receive a
  descriptor". A websocket is the same pump; after the upgrade it is
  just bytes.

The server rewrites each forwarded request's head before replaying it:
the `Cookie` header is removed — the bearer never crosses into user
space — and two headers are added, `X-Atrium-Browser-Session` (a
random tag distinct from the cookie value, letting a session tell its
browsers apart) and `X-Atrium-Client-Addr`. A session host can trust
those headers for exactly one reason: nothing but atrium-server can
reach it. The descriptors arrive over a private socketpair, the host
listens on nothing, and that property is what the whole arrangement
leans on.

atriumd watches each session host through the pidfd that peinit
returns at submission, and reads how a session ended from peinit's job
record rather than from `wait`.

---

# 3.1 Logging On

_Peios / Advanced Peios / Atrium / Logon And Sessions_

> The login page is a rendering of authd's prompts — atriumd is a PGSS Logon client with a browser for a terminal.

Atrium adds no authentication of its own. Logging on through the
browser is a PGSS Logon conversation (PGSS §2) with authd, conducted
by atriumd, with the browser standing where a terminal would.

A browser with no valid cookie gets the login page from atrium-server
— the one page the server itself owns, since before a logon there is
no session to serve anything. The page collects a username and posts
it; from there:

1. atriumd connects to `/run/logon.sock` and sends `LogonStart`,
   authored by atriumd itself: logon type Network, the username, and
   the client address as the unverified remote host. The browser
   proposes nothing.
2. authd's credential requests are relayed outward as JSON — the page
   renders whatever prompts arrive, with no assumption that the answer
   is a password — and the answers travel back the same way.
3. On `AccessGranted`, the token descriptor stays in atriumd's session
   table; it never travels further. atriumd submits the session host
   (§3.2), hands the server the session's socket, and the server mints
   a cookie.
4. On `AccessDenied`, a retryable denial is shown to the browser as
   one uniform "Incorrect username or password"; the specific denial
   goes to the log. Non-retryable denials are shown as given.

The cookie is 256 bits from the kernel's entropy pool, hex-encoded,
`HttpOnly` and `SameSite=Strict`. It is a bearer: whoever holds it is
the session, which is why it goes into the cookie jar and nowhere
else — not to session hosts, not into JavaScript. One cookie binds one
browser to one interactive session; logging out discards both the
cookie and the session.

Passwords cross the network unencrypted in this version, as does
everything else; the deployment posture is a trusted network segment
until TLS lands.

---

# 3.2 Session Hosts as Jobs

_Peios / Advanced Peios / Atrium / Logon And Sessions_

> A session host is a peinit job submitted with the logon token as its identity — Atrium never forks as anyone but itself.

On a successful logon, atriumd submits one job to
`/run/services/peinit/jobs.sock` (PSPU §7):

- **Identity**: the logon token authd just minted, attached to the
  submission as a `KACS_SCM_TOKEN`. The kernel gates the attach and
  peinit duplicates the token to the job's primary — atriumd holds the
  token but never installs it, on itself or anyone.
- **Program**: `/usr/libexec/atrium/atrium-session`, no arguments.
- **Descriptors**: the session's control socket, named
  `atrium-control`, which peinit places on descriptor 3.
- **Environment**: `HOME`, `USER`, `LOGNAME`, `PATH`, `SHELL` (from
  the logon profile; `/bin/sh` when the profile names none), plus
  `ATRIUM_SESSION` (the logon session id) and `ATRIUM_DISPLAY_NAME`.
  The working directory is the profile's home when that directory
  exists, `/` otherwise — a missing home starts a session rather than
  failing one.
- **Stop**: `stop_timeout` of 10 seconds.

The response carries the job's pidfd, which joins atriumd's poll loop;
a session host's death is noticed there, and the cause is read back
from peinit's job record while it is retained. Logging out sends the
jobs channel a `stop` — SIGTERM, then peinit's kill after the stop
timeout, cgroup-wide.

A jobs-channel connection is opened per operation and dropped: peinit
closes idle jobs connections, and a logon is rare enough that a kept
connection would be found dead exactly when it was next needed.

Because peinit is the parent, session hosts survive an atriumd
restart as processes — but atriumd's table and the server's cookies do
not, so a restarted Atrium does not know them and users log in afresh.
Reattachment through peinit's job listing is possible machinery that
this version does not have.

---

# 3.3 Lifecycle

_Peios / Advanced Peios / Atrium / Logon And Sessions_

> What ends a session, what merely detaches a browser, and how a shell that has lost its session finds its way home.

A browser closing changes nothing on the machine. The interactive
session keeps running; the browser's cookie still names it; returning
to the page resumes the same desktop. Detachment is free.

What ends an interactive session:

| Event | Path |
|---|---|
| Logout | The server drops the cookie and session link, tells atriumd, and atriumd stops the peinit job |
| The session host exits | atriumd's pidfd poll notices; the token is dropped, which releases the logon session |
| atriumd exits | atrium-server dies with it (parent-death signal); session hosts see their control socket close and exit |
| The machine reboots | Everything above |

The logon token is held by atriumd for the session's life and dropped
when the session ends; dropping the last token reference is what
retires the kernel's logon session.

On the browser side, the shell treats its websocket as expendable.
Requests made while the socket is connecting are queued and flushed on
open, so a click during the first half-second is not lost. Reconnection
backs off from 300 ms to 3 s, retries immediately when the tab regains
focus or the network returns, and every third failure asks
`/api/whoami` over plain HTTP whether the session still exists — a
stale cookie (the machine rebooted; the session was logged out
elsewhere) answers with a redirect, and the shell reloads onto the
login page rather than retrying a socket that can never connect.

---

# 4.1 The Mirror

_Peios / Advanced Peios / Atrium / The Shell_

> One websocket per tab; a snapshot, then events; requests in, broadcasts out. The session owns the state and every tab agrees.

Each browser tab's shell opens one websocket to its session host at
`/ws` (forwarded by atrium-server like any other connection). The
session sends a **snapshot** — the window list, the focus, the chord
table — and from then on **events**; the shell sends **requests**.

State flows one way. A shell never changes anything locally: clicking
a tile sends `launch`, and the tab that asked learns of the resulting
window the same way every other tab does, from the broadcast. That is
what makes two tabs agree without trying: there is nothing to
reconcile, because nothing but the session ever decides.

The session pushes facts, not markup. An event names what changed —
`window.opened`, `focus`, `window.closed`, `window.minimized`,
`window.titled` — and each shell works out the DOM consequences. The
constraint behind this is the iframe: a window's frame is a live
application, and re-parenting or re-rendering it reloads the app, so
frames are created once and only ever shown or hidden.

Requests that fail — an unknown app, a closed window — produce an
`error` message to the asking tab alone. Errors are not state, and no
other mirror hears them. The same routing carries application
request/reply traffic (§5.4): replies and streams go only to the tab
whose frame asked.

Window ids are minted by the session. Titles are session state too —
an application renaming its window (§5.3) changes every mirror's
sidebar at once.

The websocket implementation in the session host is a deliberately
small subset of RFC 6455: text, close and ping frames, client masking
required, no fragmentation or extensions. Every write to one
connection is serialised through one lock — a pong interleaving with a
broadcast mid-frame would corrupt the stream, and did, once.

---

# 4.2 Windows

_Peios / Advanced Peios / Atrium / The Shell_

> A flat window list with one focus — numbering, minimise, the title bar, and one sidebar entry per window.

The session keeps a flat list of windows and at most one focused
window. Each window belongs to an application, has a session-minted
id and a title, and is either visible or minimised.

**Opening.** `launch` names an app. If the app already has a window,
the most recent one is focused instead — one window per application is
the default, and a second is the deliberate ask `launch { new: true }`
(the context menu's "Open in new window"). Windows of one application
are numbered by the session as they multiply: "Terminal",
"Terminal 2".

**Focus.** Focusing a minimised window restores it first
(`window.restored`, then `focus`). Closing or minimising the focused
window passes focus to the most recently opened still-visible window,
or to none — and a tab showing no focused window shows the Toolbox.

**Minimise** is session state like everything else: the frame stays
loaded in every tab, hidden; the sidebar entry dims. Whether a given
tab is looking at its windows or at the Toolbox is the one piece of
state that is deliberately per-tab.

**Chrome.** The focused window sits under a bar with its
application's glyph and title on the left and two lights on the
right: yellow minimises, red closes. Each open window has its own
sidebar entry — running dot, dimmed when minimised, highlighted when
focused; middle-click closes it. Pinned applications without windows
show a single launcher entry.

An application's frame is served from the session host
(`/apps/<id>/…`), created hidden, and revealed by the SDK handshake or
by a 1.2-second grace for pages that never load the SDK (§5.3).

---

# 4.3 Keybinds

_Peios / Advanced Peios / Atrium / The Shell_

> Three tiers of chord ownership, a session-owned table, and cooperative capture inside application frames.

Keyboard chords have three owners, and the boundaries are physical
rather than chosen:

1. **The browser and OS** own what they own — `Ctrl+W`, `Ctrl+T`,
   `Alt+Tab`, `F11` on most browsers — and a web page never sees some
   of them. Atrium's defaults avoid the common ones.
2. **The shell** owns the reserved chords: global desktop actions that
   work everywhere, including while an application frame has focus.
3. **The focused application** owns everything else.

The chord table is session state, shipped in the snapshot as
`keymap`: a list of `{ chord, action }`. Chords are written as sorted
modifiers plus a key — `Ctrl+Alt+T`, key from the keyboard event's
`key` value, single characters uppercased. Actions are strings; every
action today is a shell action, and the string form is what leaves
room for application-owned globals later without a schema change.

| Chord | Action |
|---|---|
| `Ctrl+K` | `command-bar` |
| `Alt+T` | `toolbox` |
| `Alt+W` | `close-window` |
| `Alt+M` | `minimize-window` |
| `Alt+1`…`Alt+9` | `focus-window-1`…`9` |
| ``Ctrl+` `` | `cycle-window` |

In the shell's own document, a capture-phase listener matches the
table and runs the action. Inside an application frame, the shell
cannot hear keys at all — so the SDK carries the shell's half inward:
it receives the reserved chord list with `ready`, captures those
chords at its own capture phase, prevents the default, and posts the
chord back to the shell, which runs the action. Everything else
passes to the application untouched.

The arrangement is cooperative. A frame that does not load the SDK
swallows global chords while focused — the documented cost of not
integrating — and a hostile frame could at worst do the same, since
the shell acts only on chords present in its own table.

---

# 5.1 What an Application Is

_Peios / Advanced Peios / Atrium / Applications_

> One contract on two axes — where the UI comes from, and what runs behind it — and the four shapes that fall out.

An Atrium application is a directory of web files with a manifest. The
contract has two independent axes rather than kinds:

- **Where the UI comes from.** Either the package ships static files
  and the session host serves them (`entry`, the default), or the
  frame points at a URL Atrium does not serve — an application that is
  its own web server, or someone else's.
- **What runs behind it.** Nothing; commands the session runs on
  request (`exec`, §5.5); a terminal (`pty`, §5.5); or, in a later
  version, a long-running backend process.

The useful shapes fall out of the grid: a pure page (About), a
*wrapper app* — static UI whose buttons run allowlisted commands, most
of the Toolbox's natural contents — a terminal, and an embedded
external web application.

Two properties hold across all of them:

- **Everything an application does on the machine happens as the
  user**, inside the session host's process tree and the session job's
  cgroup. The manifest's capabilities narrow what an application may
  ask the session to do; they never widen who it is.
- **An application that loads the SDK is a citizen; one that does not
  still works.** The SDK is how a frame gets the handshake, the theme,
  requests and shortcuts. A plain page without it is shown after a
  grace period and swallows global chords while focused — degraded,
  deliberately, rather than broken.

---

# 5.2 The Manifest and the Catalogue

_Peios / Advanced Peios / Atrium / Applications_

> A directory whose name is its id, a manifest.toml, and a catalogue that is nothing but the directory listing.

Applications install under `/usr/share/atrium/apps/<id>/`, one
directory per application, shipped by ordinary packages. Nothing runs
at install and nothing registers: presence is the catalogue. The
session host reads the directory on every request, so an installed
package's app appears on the next Toolbox load.

The id is reverse-DNS (`org.peios.terminal`), lowercase letters,
digits, hyphens and dots, and **the directory name is the id** — a
manifest whose `id` disagrees with its directory is a broken package
and is skipped, with a line on the session's log.

`manifest.toml`:

| Field | Meaning |
|---|---|
| `id` | The application id; equal to the directory name |
| `name` | Display name |
| `description` | One line, shown on the tile |
| `category` | Grouping label for the launcher; optional |
| `icon` | A file in the directory, served as the icon; optional |
| `glyph`, `color` | A built-in stroked glyph name and chip colour, used when no icon ships |
| `entry` | The document the window loads; default `index.html` |
| `[capabilities]` | What the app may ask the session for (§5.5) |

The session host serves `/apps/<id>/<file>` from the application's
directory, rejecting any path that could escape it, and serves the
catalogue as JSON at `/api/apps` with each entry's `entry` and `icon`
already resolved to URLs.

Two stylesheets are served to every application from the shell's own
assets: `/shell/tokens.css`, the design system's variables plus body
ground defaults — an application that links it and styles with
`var(--c-*)` matches the shell in both themes with no colours of its
own — and the SDK at `/shell/sdk.js`.

---

# 5.3 The SDK

_Peios / Advanced Peios / Atrium / Applications_

> One ES module an application imports — the handshake that reveals the frame, requests and streams, shortcuts, title and theme.

`/shell/sdk.js` is a dependency-free ES module. Importing it sends
`hello` to the shell; everything else hangs off the exported `atrium`
object.

**The handshake.** A window's frame is created hidden. When the SDK's
`hello` arrives, the shell answers `ready` with the window id, the
current theme, the user (username and display name) and the reserved
chord list, and reveals the frame; `atrium.ready()` resolves with that
context. A page that never sends `hello` is revealed by a 1.2-second
grace instead. The SDK sets `data-theme` on the document root at
`ready` and keeps it current, which is what drives `tokens.css`
variables; `atrium.on('theme', fn)` hears changes.

**Requests.** `atrium.request(body)` sends one JSON object to the
session and resolves with the reply. Kinds that stream — `exec` —
deliver interim parts to an optional second argument before the final
reply. Convenience wrappers:

- `atrium.exec(cmd, onOutput)` — run an allowlisted program (§5.5);
  `onOutput(text, stream)` receives stdout and stderr as they happen;
  resolves with `{ exit_code, exit_signal, truncated }`.
- `atrium.pty({ cols, rows, onData })` — open a terminal (§5.5);
  returns `{ write, resize, done }`.

**Window verbs.** `atrium.setTitle(s)` renames the window — session
state, visible in every tab's bar and sidebar. `atrium.close()` asks
the shell to close this window.

**Shortcuts.** `atrium.shortcuts.on(chord, fn)` registers an
application-local chord, active while the window has focus. Reserved
chords never fire here; they are captured first and forwarded to the
shell (§4.3).

**The declarative layer.** For wrapper apps, the SDK wires clicks on
any element carrying `data-atrium-exec`:

```html
<button data-atrium-exec="peipkg list" data-atrium-target="#out">
  Installed packages
</button>
<pre id="out"></pre>
```

Output streams into the target as text; the exit status lands on the
target as a `data-exit` attribute; the trigger is disabled while the
command runs. The command string is split on whitespace with no
quoting — an invocation that needs quoting has outgrown the attribute
and uses `atrium.exec`.

---

# 5.4 The Bus

_Peios / Advanced Peios / Atrium / Applications_

> postMessage to the shell, an envelope the shell stamps, and replies that return to exactly the frame that asked.

An application frame holds no socket and no cookie. Its one channel is
`postMessage` to the parent shell — the only browser mechanism that
survives every isolation boundary Atrium may later put around a frame
(foreign origins, sandboxed frames with an opaque origin). The path of
a request:

```
app frame → postMessage → shell → websocket → session host
```

The shell identifies the sending frame by its `contentWindow` and
stamps the window id onto the relayed message. An application cannot
name its window and therefore cannot speak as another; the session
never authenticates applications, because the shell has already
vouched for which window is talking and the session knows which
application owns that window.

Replies and streams route the same way in reverse — to the one tab
whose frame asked, then by `postMessage` into that frame — correlated
by a request id the SDK mints per call.

The bus is a control plane. A round trip costs postMessage in and out
plus the websocket, which is invisible next to the network for
anything a human triggers, and measured throughput is comfortable for
terminal-scale streaming. The recorded posture is *control on the
bus, bytes on a channel*: an application that ever ships sustained
bulk data gets a dedicated channel then, authenticated by a
per-window token handed over the bus — machinery that exists as a
design, not in this version, because nothing has needed it.

---

# 5.5 Capabilities

_Peios / Advanced Peios / Atrium / Applications_

> The manifest's [capabilities] table — exec's allowlist, limits and streams, and pty's terminal — checked by the session at request time.

An application's manifest declares what it may ask the session to do.
The session host reads the manifest from disk at each request — the
installed manifest is the truth, and there is no registration to go
stale. An application with no `[capabilities]` table can render, talk
on the bus, and nothing else.

## 5.5.1 exec

```toml
[capabilities]
exec = ["peipkg", "svctl"]
```

`{ kind: "exec", cmd: [argv…] }` runs a program as the user. The
first argv element is matched against the list — `"*"` allows any
program — and a refusal names the application, the program and the
manifest, so a denial reads as an explanation rather than a mystery.

No shell is involved: argv is executed as given, in the user's home,
with the session's environment. Output streams back as it happens,
stdout and stderr separately labelled; the final reply carries the
exit code or signal. Two limits protect the session: one megabyte of
streamed output and five minutes of runtime, past either of which the
process is killed and the reply says `truncated`.

## 5.5.2 pty

```toml
[capabilities]
pty = true
```

`{ kind: "pty-open", cols, rows }` opens a pseudo-terminal running
the user's `$SHELL` as a session leader with the slave as its
controlling terminal, `TERM=xterm-256color`. Output streams like
exec's; input and window sizes travel as fire-and-forget
`pty-input` / `pty-resize` messages naming the opening request; the
opening request's reply arrives when the shell exits. Closing the
window hangs up the terminal's whole process group.

Capabilities narrow what may be *asked*; identity never changes.
Everything either capability runs is the user, in the session's
process tree, indistinguishable from work the user did at a console —
which is also why the model is honest: an application granted
`exec = ["*"]` has been granted the user, and a manifest is the place
that grant is visible.

---

# 6.1 Cannot Log In

_Peios / Advanced Peios / Atrium / Failure Modes_

> Where a failed browser logon actually failed — the authority, the session job, or the network path — and what each looks like.

A browser logon crosses four systems, and the error surface says which
one refused.

**"Incorrect username or password"** is authd refusing the
credentials — the uniform rendering of every retryable denial. The
specific denial is on atriumd's log (kmsg-mirrored, so the serial
console has it).

**"Could not start a session"** means the logon succeeded but the
session host did not start: atriumd's job submission failed. atriumd's
log carries peinit's refusal — `BAD_TOKEN`, a jobs-socket error, or a
failed launch, with the job's cause when one was created. The logon
token is dropped on this path, so nothing lingers.

**"Session host unavailable"** on the login page means atrium-server
could not reach atriumd at all — atriumd is down or restarting, and
peinit's view of the `atriumd` service is the place to look.

**Nothing loads at all**: nothing is listening on 8080 (the service
is down), or the network path is wrong — on a QEMU dev image the port
is user-mode-NAT forwarded and reachable only through the host's
forward, not the guest address.

A logon that succeeds but lands on a desktop that immediately bounces
back to the login page is a session that died at birth: the pidfd
noticed the host exit before the first request arrived. peinit's job
record holds the exit.

---

# 6.2 The Shell Loses the Session

_Peios / Advanced Peios / Atrium / Failure Modes_

> What a dropped websocket looks like, what the shell does about it by itself, and when it gives up and reloads.

The shell's websocket drops for ordinary reasons — the machine
rebooted, the laptop slept, the service restarted — and the shell is
built to make most of them invisible.

While disconnected, requests queue rather than drop; the browser
console notes each (`session: queued until connected`). Reconnection
backs off 300 ms → 3 s, and a tab regaining focus or the network
coming back retries immediately. Each websocket close is logged to
the console with its code, which is the first thing worth reading
when a shell seems dead.

Every third failed attempt, the shell asks `/api/whoami` whether the
session behind its cookie still exists. A redirect answer means it
does not — the machine rebooted, or the session was ended elsewhere —
and the shell reloads, landing on the login page. Without that check
a tab surviving a reboot would retry its websocket forever, since its
cookie can never bind again.

What is lost across an interactive session's death is the session:
open windows, terminals and their processes. What is never lost is
anything the browser held alone, because the browser holds nothing
alone.

If windows open but an application's frame stays blank past the
handshake grace, the failure is inside the frame — its console (the
browser's, for that frame) has the SDK's report, and the session
host's log has the serving side.

---

# 6.3 Applications That Fail

_Peios / Advanced Peios / Atrium / Failure Modes_

> Broken manifests, refused capabilities, dead commands and dead terminals — each failure names itself.

**A missing tile.** An application that should be in the Toolbox and
is not has a manifest problem: unparsable TOML, or an `id` that does
not match its directory. Both are skipped with a line on the session
host's log naming the file.

**A capability refusal.** An `exec` for a program outside the
allowlist, or a `pty-open` without `pty = true`, is answered with an
error naming the application, the request and the manifest —
delivered to the asking frame, where the SDK rejects the promise. The
declarative layer prints it into the target element; the Terminal
shows it in place of the "shell exited" banner.

**A command that dies or floods.** exec's reply distinguishes an exit
code, a signal, and `truncated: true` — the last meaning the session
killed it at the output cap or the timeout, and the record of which
is on the session log. Output already streamed stays streamed.

**A terminal that exits instantly** is the shell exiting, and the
banner carries the refusal when there was one instead of an exit. A
pseudo-terminal that cannot be opened at all reads as `openpty`
failing in the message; on Peios that has one historical cause worth
knowing — a devpts mount without its synthesised-SD policy, under
which every slave is unopenable (peinit TRM §2.3).

**One tab wrong, others right** points at the mirror rather than the
session: the state is per-session, but delivery is per-tab. A reload
of the odd tab resynchronises it from the snapshot; the console's
websocket log says what it missed.

---

# Appendix A Message Reference

_Peios / Advanced Peios / Atrium_

> The mirror websocket and the frame bus, message by message.

## A.1 The mirror websocket (`/ws`)

One JSON object per text frame. Shell → session:

| Message | Fields | Effect |
|---|---|---|
| `launch` | `app`, `new?` | Open (or focus) an application |
| `focus` | `id` | Focus a window, restoring it if minimised |
| `minimize` | `id` | Minimise a window |
| `close` | `id` | Close a window |
| `set-title` | `id`, `title` | Rename a window (relayed from its app) |
| `app` | `win`, `req`, `body` | An application request, shell-stamped |

Session → shell:

| Message | Fields | Audience |
|---|---|---|
| `snapshot` | `windows`, `focus`, `keymap` | The connecting tab |
| `window.opened` | `window` | Every tab |
| `window.closed` | `id` | Every tab |
| `window.minimized` / `window.restored` | `id` | Every tab |
| `window.titled` | `id`, `title` | Every tab |
| `focus` | `id` (nullable) | Every tab |
| `app.reply` | `win`, `req`, `body` | The asking tab |
| `app.stream` | `win`, `req`, `body` | The asking tab |
| `error` | `message` | The asking tab |

## A.2 The frame bus (postMessage)

App → shell: `hello {v}` · `set-title {title}` · `close` ·
`request {req, body}` (`req` 0 marks fire-and-forget: `pty-input`,
`pty-resize`) · `chord {chord}` (a captured reserved chord).

Shell → app: `ready {window, theme, user, reserved}` ·
`theme {theme}` · `reply {req, body | error}` · `stream {req, body}`.

## A.3 Application request kinds

| Kind | Capability | Reply |
|---|---|---|
| `system-info` | — | Hostname, kernel, uptime, user, logon session, host pid, Atrium version |
| `exec` | `exec` allowlist | Streamed output, then `{exit_code, exit_signal, truncated}` |
| `pty-open` | `pty` | Streamed output; reply on shell exit |
| `pty-input`, `pty-resize` | `pty` | None (fire-and-forget) |

---

# Appendix B Constants and Paths

_Peios / Advanced Peios / Atrium_

> Ports, sockets, filesystem locations, environment, and limits, in one place.

## B.1 Network and sockets

| What | Value |
|---|---|
| Listener | TCP `0.0.0.0:8080`, plain HTTP (atrium-server) |
| Mirror websocket | `GET /ws` on the same origin |
| Control descriptors | Descriptor 3 in atrium-server and atrium-session |
| Logon authority | `/run/logon.sock` (client) |
| Jobs channel | `/run/services/peinit/jobs.sock` (client, one connection per operation) |

## B.2 Filesystem

| What | Where |
|---|---|
| Binaries | `/usr/sbin/atriumd`, `/usr/libexec/atrium/atrium-server`, `/usr/libexec/atrium/atrium-session` |
| Applications | `/usr/share/atrium/apps/<id>/manifest.toml` |
| Service definition | `/usr/share/regim/atriumd-service.reg` (applied by an image's autoapply) |
| Shell assets | Served at `/shell/…`: `shell.js`, `shell.css`, `icons.js`, `sdk.js`, `tokens.css` — compiled into atrium-session |

## B.3 Session host environment

`HOME`, `USER`, `LOGNAME`, `PATH`, `SHELL`, `ATRIUM_SESSION` (logon
session id), `ATRIUM_DISPLAY_NAME`. Applications' exec children
inherit the session host's environment.

## B.4 Limits and defaults

| What | Value |
|---|---|
| Cookie | 256-bit random, `HttpOnly`, `SameSite=Strict` |
| exec output cap | 1 MiB, then kill + `truncated` |
| exec runtime cap | 300 s |
| Frame handshake grace | 1.2 s |
| Websocket reconnect | 300 ms × failures, capped 3 s; liveness check each 3rd failure |
| Session job stop timeout | 10 s |
| Window title cap | 120 characters |
| pty terminal type | `xterm-256color` |

## B.5 Development overrides

`ATRIUM_LISTEN`, `ATRIUM_APPS_DIR`, `ATRIUM_SERVER`, `ATRIUM_SESSION`
(binary path), `ATRIUM_JOBS_SOCKET`; `atriumd --unrestricted` runs
the whole stack under one identity on a KACS-less host. The `atrium`
source tree's `tools/dev/` holds the fake-atriumd harness and browser
drivers these serve.
