539 lines
29 KiB
Markdown
539 lines
29 KiB
Markdown
# TUI Service Menu — Post-Identity Service Selection
|
|
|
|
> **Status:** Design. This document extends [`plans/tui_login.md`](tui_login.md) with a second screen inside the same `nostr-id-tui` binary: after the user has successfully loaded an identity (mnemonic / generated / imported / bunker / amnesia), the TUI presents a menu of **optional services** and lets the user toggle each one on or off **before** `getty@tty1` is allowed to start.
|
|
>
|
|
> Depends on: [`plans/tui_login.md`](tui_login.md), [`plans/identity_subsystem.md`](identity_subsystem.md), [`plans/iso_architecture.md`](iso_architecture.md), [`plans/nostr_config_projection.md`](nostr_config_projection.md), [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md).
|
|
|
|
## 0. Policy
|
|
|
|
1. **n-OS-tr always installs and sets up every service.** Binaries, unit files, config templates, data directories — all present on the image.
|
|
2. **No service starts automatically at boot except the identity/boot pipeline itself.** Exactly these three are enabled image-wide: [`n-os-tr-firstboot.service`](../iso/config/includes.chroot/etc/systemd/system/n-os-tr-firstboot.service), `nostr-id.service` (identity-agent), [`nostr-id-tui.service`](../iso/config/includes.chroot/etc/systemd/system/nostr-id-tui.service). Everything else ships `disabled` via the live-build preset policy.
|
|
3. **The user decides what turns on.** After the user authenticates via `nostr-id-tui`, the TUI presents a service-toggle screen. The user picks what they want running this session.
|
|
4. **Selections are session-local in v1.** No persistence across reboots. Every boot starts with the same defaults. A future revision may project selections to a Nostr config slot (§10).
|
|
5. **One binary, one ceremony.** The service menu is a new screen inside `nostr-id-tui`, not a separate unit. Rationale discussed in the architect session: the user is already in one authenticated terminal flow; we do not want two back-to-back ceremonies before login.
|
|
6. **Default-off by default.** Every toggleable service starts the session in the "off" state. The user must explicitly enable each one they want running. This matches the privacy-forward posture of the project.
|
|
7. **Style:** strict adherence to [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md) — single-key actions, terminal-width headers, ANSI underline for hotkeys, canonical menu template (§12 of that doc).
|
|
|
|
## 1. Scope
|
|
|
|
### 1.1 Services in the menu (v1)
|
|
|
|
| Key | Label | systemd unit | Default | What it does |
|
|
|-----|-----------------|---------------------------|---------|--------------------------------------------------------|
|
|
| `r` | `c-relay` | `c-relay.service` | off | Local Nostr relay (NIP-01/42). Admin key = index 2. |
|
|
| `g` | `ginxsom` | `ginxsom.service` | off | Blossom blob server behind nginx. Server key = index 3.|
|
|
| `f` | `fips` | `fips.service` | off | Mesh-routing daemon; npub-IPv6 overlay. Key = index 4. |
|
|
| `n` | `nginx` | `nginx.service` | off | TLS front door / reverse proxy. Needed by c-relay ws, ginxsom uploads. |
|
|
| `l` | `config-loader` | `nostr-config-loader.service` | off | Fetch + materialize user config slots (§ [`nostr_config_projection.md`](nostr_config_projection.md#3-boot-flow)). |
|
|
| `w` | `config-writer` | `nostr-config-writer.service` | off | Save tracked home paths back to Nostr on shutdown. |
|
|
| `s` | `ssh` | `ssh.service` | off | OpenSSH server. Keys regenerated at firstboot. |
|
|
| `t` | `tor` | `tor.service` | off | Tor daemon. Default posture = SOCKS-only (client), not exit/relay. |
|
|
|
|
### 1.2 Services explicitly NOT in the menu
|
|
|
|
- `nostr-id.service` — identity-agent is always required; it's the thing the TUI talks to.
|
|
- `n-os-tr-firstboot.service` — oneshot, runs before the TUI, not user-controlled.
|
|
- `nostr-id-tui.service` — the TUI itself.
|
|
- `systemd-networkd`/`NetworkManager`/`dbus` — core system plumbing; managed by distro defaults.
|
|
|
|
### 1.3 Service dependencies the TUI must respect
|
|
|
|
Some services only make sense together. The TUI enforces these dependencies at toggle time (not at the systemd level — systemd's own `Requires=`/`Wants=` handles the actual startup ordering):
|
|
|
|
| Service | Implicit deps (auto-enabled when this is turned on) |
|
|
|---------------|-------------------------------------------------------------|
|
|
| `c-relay` | `nginx` (wss termination and cert) |
|
|
| `ginxsom` | `nginx` (serves blobs, proxies uploads) |
|
|
| `config-writer` | `config-loader` (must have run at boot to know the slots) |
|
|
|
|
If the user toggles `c-relay` on and `nginx` is off, the TUI:
|
|
- Shows a one-line prompt: `c-relay needs nginx. Turn nginx on too? [Y/n]`.
|
|
- Default `y` on Enter. If `n`, the toggle is cancelled (we do not start a service that won't be reachable).
|
|
|
|
Reverse direction (turning `nginx` off while `c-relay` is on): prompt `c-relay will become unreachable without nginx. Stop c-relay too? [Y/n]`.
|
|
|
|
## 2. Placement in the existing TUI flow
|
|
|
|
### 2.1 Updated state diagram
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Menu
|
|
|
|
Menu --> Enter: 1 / E
|
|
Menu --> Generate: 2 / G
|
|
Menu --> Import: 3 / I
|
|
Menu --> Bunker: 4 / B
|
|
Menu --> Amnesia: 5 / A
|
|
|
|
Enter --> SendAgent: BIP-39 ok
|
|
Generate --> SendAgent: confirm words ok
|
|
Import --> SendAgent: file read + wiped
|
|
Bunker --> SendAgent: NIP-46 connected
|
|
Amnesia --> SendAgent: ephemeral key
|
|
|
|
SendAgent --> Services: agent returns ok
|
|
SendAgent --> FatalError: agent returns error
|
|
|
|
Services --> Services: user toggles
|
|
Services --> Confirm: user presses C (commit)
|
|
Services --> Services: user presses X (skip, all off)
|
|
|
|
Confirm --> Apply: user presses Y
|
|
Confirm --> Services: user presses N
|
|
|
|
Apply --> Apply: start/stop each selected unit
|
|
Apply --> Exit: all units reached terminal state
|
|
Apply --> ApplyError: any unit failed
|
|
|
|
ApplyError --> Services: user reviews, retries
|
|
|
|
FatalError --> [*]
|
|
Exit --> [*]
|
|
```
|
|
|
|
The only new states are `Services`, `Confirm`, `Apply`, and `ApplyError`. Everything before `SendAgent` is unchanged from [`plans/tui_login.md §3.1`](tui_login.md#31-state-diagram).
|
|
|
|
### 2.2 Updated boot sequence
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant SD as systemd
|
|
participant IA as nostr-id.service
|
|
participant TUI as nostr-id-tui.service
|
|
participant Svc as toggleable services
|
|
participant GT as getty@tty1.service
|
|
|
|
SD->>IA: start
|
|
IA-->>SD: listening on control.sock
|
|
SD->>TUI: start (owns tty1)
|
|
Note over TUI: identity screens
|
|
TUI->>IA: load_mnemonic/bunker/amnesia
|
|
IA-->>TUI: ok, main_npub
|
|
Note over TUI: service menu
|
|
TUI->>TUI: user toggles, presses Commit
|
|
TUI->>SD: StartUnit/StopUnit via dbus
|
|
SD->>Svc: start selected services
|
|
Svc-->>SD: active
|
|
SD-->>TUI: job results
|
|
TUI->>SD: exit 0
|
|
SD->>GT: start (ordering allows)
|
|
GT->>User: n-os-tr login:
|
|
```
|
|
|
|
## 3. Screen design
|
|
|
|
Strict compliance with [`/home/user/lt/aesthetics/TUI.md`](/home/user/lt/aesthetics/TUI.md). Every screen follows the canonical template in §12 of that doc.
|
|
|
|
### 3.1 Main services screen
|
|
|
|
```text
|
|
===================== SERVICES =====================
|
|
(expands to terminal width)
|
|
|
|
Identity: npub1abcd…wxyz (main, index 0)
|
|
Session: live (in-memory, no persistence)
|
|
|
|
Key Service State Default Depends on
|
|
--- ------------- --------- ------- -----------
|
|
R c-relay [ off ] off nginx
|
|
G ginxsom [ off ] off nginx
|
|
F fips [ off ] off —
|
|
N nginx [ off ] off —
|
|
L config-loader [ off ] off —
|
|
W config-writer [ off ] off config-loader
|
|
S ssh [ off ] off —
|
|
T tor [ off ] off —
|
|
|
|
Actions:
|
|
R/G/F/N/L/W/S/T) Toggle service
|
|
A) Turn All on
|
|
Z) Turn all off (reset)
|
|
C) Commit and continue
|
|
X) Skip (all off, continue)
|
|
|
|
Select action: >
|
|
```
|
|
|
|
Formatting rules (all from [TUI.md](/home/user/lt/aesthetics/TUI.md)):
|
|
|
|
- Header expands to full terminal width using the `=== SERVICES ===` pattern from [`ui.c:print_centered_header`](/home/user/lt/aesthetics/TUI.md:88).
|
|
- Column widths are fixed; when terminal is narrower than 60 cols, drop the "Default" and "Depends on" columns (the state is what matters).
|
|
- State column is either `[ off ]` or `[ on ]`. A third transient state `[ ... ]` appears during the Apply phase.
|
|
- The first letter of each service name in the Key column is rendered with ANSI underline via `\033[4m<letter>\033[0m`, matching [TUI.md §7](/home/user/lt/aesthetics/TUI.md).
|
|
- Prompt at bottom is `Select action: >` per the canonical template.
|
|
|
|
### 3.2 Confirm screen
|
|
|
|
Before actually doing anything, we show what will happen:
|
|
|
|
```text
|
|
===================== CONFIRM =====================
|
|
|
|
The following changes will be applied:
|
|
|
|
START nginx.service
|
|
START c-relay.service
|
|
START fips.service
|
|
|
|
(5 services remain off)
|
|
|
|
Proceed? [Y/n] >
|
|
```
|
|
|
|
Only services whose session state differs from their default appear. Pure-default sessions (no changes) go straight from the services screen's `C`/`X` directly to `Apply` with an empty plan and exit.
|
|
|
|
### 3.3 Apply screen
|
|
|
|
Live progress. We iterate the plan serially (not concurrently — systemd handles that anyway, and serial output is easier to read):
|
|
|
|
```text
|
|
===================== APPLYING =====================
|
|
|
|
[ OK ] nginx.service started in 0.42s
|
|
[ OK ] c-relay.service started in 1.14s
|
|
[ ... ] fips.service starting...
|
|
[ ] (waiting)
|
|
|
|
Press Ctrl-C to abort remaining jobs (already-started services stay up).
|
|
```
|
|
|
|
Each row shows one of:
|
|
- `[ ]` queued
|
|
- `[ ... ]` in progress (animated dot after 250 ms; ANSI cursor save/restore to re-render only the affected row)
|
|
- `[ OK ]` finished successfully
|
|
- `[FAIL ]` finished with an error
|
|
- `[SKIP ]` not applied (e.g. dependency refused earlier)
|
|
|
|
After all jobs complete:
|
|
|
|
- All green → advance to Exit immediately (print `All services started. Handing control to login...` for 1 second, then the TUI exits and getty takes over).
|
|
- Any failures → advance to **ApplyError** screen.
|
|
|
|
### 3.4 Apply-error screen
|
|
|
|
```text
|
|
===================== PARTIAL FAILURE =====================
|
|
|
|
Some services did not start cleanly:
|
|
|
|
[FAIL ] ginxsom.service
|
|
SubState: failed
|
|
ExecMainStatus: 1
|
|
Last log line: "/usr/local/bin/ginxsom/ginxsom-fcgi: socket bind failed"
|
|
|
|
Actions:
|
|
V) View journal for failed services
|
|
R) Retry failed services only
|
|
B) Back to service menu
|
|
I) Ignore and continue to login anyway
|
|
|
|
Select action: >
|
|
```
|
|
|
|
The journal view (`V`) shells out to `journalctl -u <unit> -n 50 --no-pager` via a pipe (we do not embed a log viewer). `less`-style paging is acceptable if available; otherwise we just print and prompt to continue.
|
|
|
|
## 4. Service manifest
|
|
|
|
The TUI does not hard-code the service list in C. It reads `/etc/n-os-tr/services.conf` at startup. This lets us add/remove services (especially tor, which lands later) without rebuilding the binary.
|
|
|
|
### 4.1 File format
|
|
|
|
A small line-based format, one record per service, fields tab-separated:
|
|
|
|
```
|
|
# key unit label default deps
|
|
r c-relay.service c-relay off nginx.service
|
|
g ginxsom.service ginxsom off nginx.service
|
|
f fips.service fips off -
|
|
n nginx.service nginx off -
|
|
l nostr-config-loader.service config-loader off -
|
|
w nostr-config-writer.service config-writer off nostr-config-loader.service
|
|
s ssh.service ssh off -
|
|
t tor.service tor off -
|
|
```
|
|
|
|
Comments start with `#`. Blank lines ignored. Unknown fields ignored for forward compatibility. Multiple deps separated by `,`. A literal `-` means no deps.
|
|
|
|
Parsing happens in `stack/nostr-id-tui/src/services_conf.c`. On parse error the TUI falls back to a hardcoded minimal list (c-relay, ginxsom, fips, nginx) and logs the parse error to stderr (captured by journald, per the service unit's `StandardError=journal`).
|
|
|
|
### 4.2 Install location
|
|
|
|
Shipped in the chroot at [`iso/config/includes.chroot/etc/n-os-tr/services.conf`](../iso/config/includes.chroot/etc/n-os-tr/services.conf). Mode `0644`, owner `root:root`.
|
|
|
|
## 5. Control path: talking to systemd
|
|
|
|
### 5.1 Transport choice
|
|
|
|
The TUI needs to:
|
|
1. Query each unit's current state (`LoadState`, `ActiveState`, `SubState`).
|
|
2. Start and stop units.
|
|
3. Receive job-completion signals.
|
|
|
|
Two realistic options in a static musl C binary with no glibc:
|
|
|
|
| Option | Notes | Verdict |
|
|
|----------------------------------------------------------------|---------------------------------------------------------------------------------------------|---------------------|
|
|
| **Shell out to `systemctl`** via `fork` + `execve` | Simple. Exit codes are well-defined. Parse `show --property=ActiveState=`. No linking. | **v1 default.** |
|
|
| **Speak the D-Bus wire protocol directly** to `/run/dbus/system_bus_socket` | Avoids forking. Enables real-time `JobRemoved` signals. Needs a tiny D-Bus marshaller (~1 KLOC) or the vendored cjson-style lib. | Later optimization. |
|
|
| **Link `libsystemd.a`** | Would pull in a chunk of systemd source into Alpine build, and libsystemd isn't available as `-static` in Alpine. | Rejected. |
|
|
|
|
v1 uses `systemctl`. The binary path is hardcoded to `/bin/systemctl` (present on every Debian live image — `systemctl` itself is part of `systemd` which is in base). The wrapper module is `stack/nostr-id-tui/src/systemd_client.c` and exposes:
|
|
|
|
```c
|
|
typedef enum {
|
|
SVC_INACTIVE = 0,
|
|
SVC_ACTIVATING,
|
|
SVC_ACTIVE,
|
|
SVC_DEACTIVATING,
|
|
SVC_FAILED,
|
|
SVC_UNKNOWN
|
|
} svc_state_t;
|
|
|
|
svc_state_t systemd_active_state(const char *unit); /* systemctl show -p ActiveState */
|
|
int systemd_start(const char *unit, int timeout_ms, char *err_out, size_t err_len);
|
|
int systemd_stop (const char *unit, int timeout_ms, char *err_out, size_t err_len);
|
|
int systemd_journal_tail(const char *unit, int n_lines, FILE *out); /* exec journalctl */
|
|
```
|
|
|
|
Each call spawns `systemctl` / `journalctl` with `posix_spawn` into a pipe, waits with a timeout, captures stdout/stderr, and returns.
|
|
|
|
### 5.2 Privilege
|
|
|
|
`nostr-id-tui.service` currently runs as root on `tty1` (see the service unit). That gives it direct `systemctl` privilege — no polkit rule needed. This matches the existing threat-model stance in [`plans/tui_login.md §6.1`](tui_login.md#61-etcsystemdsystemnostr-id-tuiservice): the TUI is already privileged because it owns the tty and talks to identity-agent over a root-only socket.
|
|
|
|
If we ever demote the TUI to run as the `nostr-id` user, we'll need `/etc/polkit-1/rules.d/50-n-os-tr-services.rules` granting that user the `org.freedesktop.systemd1.manage-units` action **only for the whitelisted units listed in `services.conf`**. That's future work.
|
|
|
|
### 5.3 Timeouts and cancellation
|
|
|
|
- Per-unit start/stop timeout: 30 s (covers nginx startup, fips mesh ready, etc.). Larger than any real service needs.
|
|
- TUI-level overall budget: 2 minutes. If exceeded, Apply falls through to ApplyError with any unfinished jobs marked `[TIME]` and the user is prompted.
|
|
- Ctrl-C in the Apply screen sends SIGINT to the current `systemctl` child, marks the remaining jobs `[SKIP ]`, and goes to ApplyError.
|
|
|
|
## 6. Source tree changes
|
|
|
|
Extending the layout declared in [`plans/tui_login.md §2.2`](tui_login.md#22-code-that-lives-in-this-repo-n_os_tr):
|
|
|
|
```
|
|
stack/nostr-id-tui/
|
|
├── src/
|
|
│ ├── main.c # now dispatches identity→services→exit
|
|
│ ├── screen_menu.c # unchanged
|
|
│ ├── screen_enter.c # unchanged
|
|
│ ├── screen_generate.c # unchanged
|
|
│ ├── screen_import.c # unchanged
|
|
│ ├── screen_bunker.c # unchanged
|
|
│ ├── screen_services.c # NEW: main service toggle screen
|
|
│ ├── screen_services_confirm.c # NEW: confirm + apply screens
|
|
│ ├── services_conf.c # NEW: parser for /etc/n-os-tr/services.conf
|
|
│ ├── systemd_client.c # NEW: fork/exec systemctl + journalctl
|
|
│ ├── curses_helpers.c # unchanged
|
|
│ ├── agent_client.c # unchanged
|
|
│ └── memhygiene.c # unchanged
|
|
├── include/
|
|
│ └── n_os_tr_tui.h # adds svc_* decls + ui_backend hooks
|
|
└── tests/
|
|
├── test_screen_services.c # NEW: state-machine unit test
|
|
├── test_services_conf.c # NEW: parser fuzz/table tests
|
|
├── test_systemd_client.c # NEW: runs against a fake systemctl shim
|
|
└── test_integration.sh # extended to toggle a service in QEMU
|
|
```
|
|
|
|
No ncurses API leaks into the new state machines. All rendering goes through the same `ui_backend_t` abstraction established in [`plans/tui_login.md §7`](tui_login.md#7-per-flavor-abstraction). The Pi LCD port will get a stripped service menu for free.
|
|
|
|
## 7. Systemd preset policy
|
|
|
|
### 7.1 What "installed but not enabled" means
|
|
|
|
For each toggleable unit we ship:
|
|
- The unit file in `iso/config/includes.chroot/etc/systemd/system/<unit>` or (for distro-packaged units like `ssh`, `tor`, `nginx`) in the package itself.
|
|
- **No symlink in `multi-user.target.wants/`** — i.e. no `systemctl enable`.
|
|
- A preset file that explicitly forbids enabling at image-build time.
|
|
|
|
### 7.2 Preset file
|
|
|
|
New file: [`iso/config/includes.chroot/etc/systemd/system-preset/50-n-os-tr.preset`](../iso/config/includes.chroot/etc/systemd/system-preset/50-n-os-tr.preset):
|
|
|
|
```
|
|
# n-OS-tr service posture: only the identity/boot pipeline auto-starts.
|
|
# Everything else is explicitly disabled. The TUI turns them on at the
|
|
# user's request after identity load.
|
|
enable nostr-id.service
|
|
enable nostr-id-tui.service
|
|
enable n-os-tr-firstboot.service
|
|
|
|
disable c-relay.service
|
|
disable ginxsom.service
|
|
disable fips.service
|
|
disable nginx.service
|
|
disable nostr-config-loader.service
|
|
disable nostr-config-writer.service
|
|
disable ssh.service
|
|
disable tor.service
|
|
```
|
|
|
|
The `50-` prefix lands before Debian defaults (typically `90-*.preset`), so our `disable` lines override e.g. nginx's upstream preset-enable.
|
|
|
|
### 7.3 Rework of the enable-services hook
|
|
|
|
Current hook [`iso/config/hooks/live/0020-enable-services.hook.chroot`](../iso/config/hooks/live/0020-enable-services.hook.chroot) enables the services directly. Rework it to be explicit about the new posture:
|
|
|
|
```sh
|
|
#!/bin/sh
|
|
set -eu
|
|
|
|
# Enable the always-on trio
|
|
systemctl enable n-os-tr-firstboot.service
|
|
systemctl enable nostr-id.service
|
|
systemctl enable nostr-id-tui.service
|
|
|
|
# Explicitly disable everything else in the service manifest.
|
|
# Idempotent; safe to re-run. `systemctl disable` is a no-op for units
|
|
# that aren't enabled yet, which is what we want.
|
|
for unit in \
|
|
c-relay.service \
|
|
ginxsom.service \
|
|
fips.service \
|
|
nginx.service \
|
|
nostr-config-loader.service \
|
|
nostr-config-writer.service \
|
|
ssh.service \
|
|
tor.service ; do
|
|
systemctl disable "$unit" 2>/dev/null || true
|
|
done
|
|
|
|
# Run the preset policy for anything we didn't list above (defensive).
|
|
systemctl preset-all --preset-mode=enable-only 2>/dev/null || true
|
|
```
|
|
|
|
### 7.4 Package-list additions
|
|
|
|
`tor` is new to the image. Add to [`iso/config/package-lists/base.list.chroot`](../iso/config/package-lists/base.list.chroot):
|
|
|
|
```
|
|
tor
|
|
```
|
|
|
|
No other new packages: nginx, openssh-server, ca-certificates, systemd, etc. are already pulled.
|
|
|
|
## 8. Interaction with identity modes
|
|
|
|
### 8.1 Normal, Bunker, and Amnesia modes
|
|
|
|
- **Normal (mnemonic loaded)**: full menu, full defaults as in §1.1.
|
|
- **Bunker mode**: services that require a raw local nsec are **disabled in the menu** (greyed out with `[ n/a ]` in the state column). Those are:
|
|
- `c-relay` — admin key comes from identity-agent via NIP-46 signing for event-based config only; for the admin nsec path the service fails. The menu shows `[ n/a ]` with a tooltip line "requires local key; bunker mode cannot provide".
|
|
- `ginxsom` — same. Key material is needed at process start (`--server-privkey` in [`ginxsom.service`](../iso/config/includes.chroot/etc/systemd/system/ginxsom.service)).
|
|
- `fips` — same.
|
|
- `nginx`, `config-loader`, `config-writer`, `ssh`, `tor` — available in bunker mode.
|
|
- **Amnesia mode**: all services available, but `config-loader`/`config-writer` are explicitly disabled with label "amnesia session — config not persisted" (we don't pull anything from Nostr for an ephemeral identity).
|
|
|
|
The rules live in `services_conf.c` as a small matrix; the file format in §4.1 can be extended later with `modes=normal,bunker,amnesia` but v1 hardcodes the matrix in C.
|
|
|
|
### 8.2 Re-entry
|
|
|
|
A future option from the Services menu: `Q) Reset identity and return to identity menu`. Deferred — not in v1. For v1 the only path out of Services is `C` (commit) or `X` (skip), both of which lead to Exit.
|
|
|
|
## 9. Testing
|
|
|
|
### 9.1 Unit (in Alpine/musl builder)
|
|
|
|
- `test_services_conf` — feeds valid and malformed manifests, asserts parser produces correct structs and correct fallback on bad input.
|
|
- `test_systemd_client` — sets `$PATH` to a directory with a shim `systemctl` that records its argv and emits canned output; asserts our client parses `ActiveState=active|inactive|failed` correctly and times out when the shim sleeps.
|
|
- `test_screen_services` — drives the services state machine through a mock `ui_backend_t`; asserts:
|
|
- Default state is all-off.
|
|
- Toggling `R` then `G` then `R` ends with `G` on and `R` off.
|
|
- Toggling `R` prompts for `N` enable; answering `y` enables both.
|
|
- `Z` (reset) returns to defaults.
|
|
- `X` (skip) goes directly to Apply with an empty plan.
|
|
- `C` (commit) goes to Confirm.
|
|
|
|
### 9.2 Integration (QEMU)
|
|
|
|
Extend [`stack/nostr-id-tui/tests/test_integration.sh`](../stack/nostr-id-tui/tests/test_integration.sh) with a new scripted run:
|
|
1. Boot the ISO.
|
|
2. At the identity screen, feed the Trezor test-vector mnemonic.
|
|
3. At the services screen, press `N` (enable nginx), then `C`, then `Y`.
|
|
4. After the TUI exits and getty appears, log in as the default user.
|
|
5. Assert `systemctl is-active nginx` = `active` and `systemctl is-active c-relay` = `inactive`.
|
|
|
|
### 9.3 Smoketest extensions
|
|
|
|
Extend [`iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest) to also verify:
|
|
|
|
- Preset posture at **image build time**: for each toggleable unit, `systemctl is-enabled <unit>` returns `disabled`.
|
|
- The manifest file `/etc/n-os-tr/services.conf` exists and parses (re-invoke the parser from a test helper).
|
|
- `/bin/systemctl` is present (the TUI depends on it).
|
|
|
|
## 10. Non-persistence and the future Nostr slot
|
|
|
|
v1: selections are session-local. Reboot → all services back to off, user re-selects.
|
|
|
|
Future: one new entry in the manifest scheme from [`plans/nostr_config_projection.md §2.1`](nostr_config_projection.md#21-slots):
|
|
|
|
```json
|
|
{
|
|
"name": "n-os-tr/services",
|
|
"path": "/run/nostr-id/services.state.json",
|
|
"mode": "0600",
|
|
"encrypted": true
|
|
}
|
|
```
|
|
|
|
Content schema (draft):
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"enabled": ["nginx.service", "c-relay.service", "fips.service"]
|
|
}
|
|
```
|
|
|
|
When a session starts, if a services slot exists, the TUI pre-loads the toggle state from it. User still has to press `C` to commit — we don't auto-apply without the user seeing the menu. This preserves the "the user decides when to turn them on" rule.
|
|
|
|
Out of scope for v1 implementation.
|
|
|
|
## 11. Failure modes
|
|
|
|
| Failure | Behavior |
|
|
|-------------------------------------------------|--------------------------------------------------------------------------|
|
|
| `/etc/n-os-tr/services.conf` missing or unparseable | Log to stderr, fall back to hardcoded minimal list (c-relay, ginxsom, fips, nginx). |
|
|
| `/bin/systemctl` missing | FatalError (as in [`plans/tui_login.md §9`](tui_login.md#9-failure-modes-summary)). The ISO is malformed. |
|
|
| Unit start returns non-zero | Row shows `[FAIL ]`, ApplyError screen is reached after all jobs settle. |
|
|
| Unit start hangs past 30 s | Timeout, row shows `[TIME]`, ApplyError. |
|
|
| User hits Ctrl-C during Apply | SIGINT to current `systemctl`, remaining jobs `[SKIP ]`, ApplyError. |
|
|
| Bunker mode, user tries to toggle `c-relay` | Toggle is ignored; status line shows "requires local key; bunker mode". |
|
|
| User toggles all off and presses Commit | Apply plan is empty; TUI prints `No services requested. Handing control to login...` and exits. |
|
|
| Identity agent dies during service menu | We're not calling the agent during this phase — selections are in TUI memory only. User is unaffected until they finish service menu and the TUI exits; if the agent is dead when services start, those services will fail on their own dependency chain. |
|
|
|
|
## 12. Docs to update
|
|
|
|
- [`plans/tui_login.md`](tui_login.md) — add a one-line reference in §3.1 to the new `Services` state after `SendAgent`; add `screen_services.c`, `screen_services_confirm.c`, `services_conf.c`, `systemd_client.c` to the source tree listing in §2.2; update §13 implementation order with the service-menu steps.
|
|
- [`plans/iso_architecture.md`](iso_architecture.md) — update §4 boot sequence to show the identity+services TUI phase before the parallel `fips/c-relay/ginxsom/nginx` block, and note that those services are started by the TUI, not auto-started by systemd.
|
|
- [`iso/config/includes.chroot/usr/share/doc/n-os-tr/README`](../iso/config/includes.chroot/usr/share/doc/n-os-tr/README) — describe the new login→services flow for end users.
|
|
- [`docs/RUNNING.md`](../docs/RUNNING.md) — add a section showing the service menu and how to toggle.
|
|
|
|
## 13. Implementation order (Phase 3a.2)
|
|
|
|
Small, independently verifiable steps. Each one leaves the repo in a runnable state.
|
|
|
|
1. **Land the preset file and rework the enable hook.** After this commit, a fresh ISO boots to a system where only `nostr-id`, `nostr-id-tui`, and `firstboot` are enabled; every other service is present on disk but inactive. Verify with `systemctl list-unit-files --state=disabled` in a QEMU boot.
|
|
2. **Add the service manifest file.** Ship `/etc/n-os-tr/services.conf` with the v1 table from §1.1. No TUI change yet; verifies that the file reaches the image.
|
|
3. **Add `tor` to the base package list.** Confirm the built ISO includes `tor.service` as a disabled unit.
|
|
4. **Implement `services_conf.c` + tests.** Parser plus a 40-line sanity test in the Alpine builder.
|
|
5. **Implement `systemd_client.c` + tests.** Fork/exec + pipe capture + shim-based test.
|
|
6. **Implement `screen_services.c` state machine + `test_screen_services`.** No rendering yet — verifies transitions against a mock backend.
|
|
7. **Render the services screen in `ui_backend_curses.c`.** First visible new screen.
|
|
8. **Implement `screen_services_confirm.c` — confirm/apply/apply-error screens.** Wire them to `systemd_client`.
|
|
9. **Hook into `main.c`.** After `agent_client` returns from `load_mnemonic`/`load_bunker`/`load_amnesia` with ok, transition to the services screen. Skip the screen entirely if `/etc/n-os-tr/services.conf` is missing *and* fallback is disabled via a `--no-services` flag (useful for tests that want only the identity flow).
|
|
10. **Extend the QEMU integration test.** Script a toggle of one service, assert post-login that the chosen service is active and the others are not.
|
|
11. **Extend the smoketest.** Add the preset-state check.
|
|
12. **Doc passes.** Update [`plans/tui_login.md`](tui_login.md), [`plans/iso_architecture.md`](iso_architecture.md), [`docs/RUNNING.md`](../docs/RUNNING.md), and the in-ISO README.
|
|
|
|
## 14. Open questions
|
|
|
|
- **`tor.service` posture out of the box.** We ship the distro default `torrc` (SOCKS 9050 only, no relay). Do we want a second unit drop-in that enables the hidden-service onion for nginx when the user turns both on? Probably yes, as a follow-up; keep v1 minimal.
|
|
- **GUI flavor.** Image flavor with a GUI will run the TUI on a text VT during early boot. Does it also allow the user to re-run the services menu later from the GUI? Probably yes via a GTK wrapper around the same `systemd_client` module, but that's post-v1.
|
|
- **Per-user vs system.** All services here are system-level. "Which user's `ssh/authorized_keys` does `ssh` serve" is answered by the config-loader slot scheme. No per-user service toggles in v1.
|
|
- **Should toggling the menu's state affect *default* behavior for future reboots?** Today: no; v1 is stateless. A future Nostr slot (§10) could record the last committed state and let the TUI show it as a "remembered" default, but that's explicit future work.
|