# Integrating `n_signer` into n-OS-tr
> **Status:** Design (revised for `n_signer` v0.0.16 `--preapprove` model). Defines how the vendored [`includes/n_signer/`](../includes/n_signer/) program becomes the boot-time mnemonic-entry + signer for n-OS-tr, replacing the previously-planned `identity-agent` daemon, and how the user gets a working multi-terminal experience without going GUI.
>
> Supersedes the daemon design in [`plans/identity_subsystem.md`](identity_subsystem.md). The mnemonic-entry portion of [`plans/tui_login.md`](tui_login.md) is also superseded; the **post-unlock service menu** part of that doc remains valid (now hosted by [`stack/nostr-id-tui/`](../stack/nostr-id-tui/) on tty2).
>
> Companion docs: [`plans/N_OS_TR_AGENT_CHANGES.md`](N_OS_TR_AGENT_CHANGES.md) (operational guide for the v0.0.16 approval model), [`plans/binary_handling_strategy.md`](binary_handling_strategy.md) (how all binaries get into the ISO), [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md) (open upstream features still needed).
## 1. Decisions locked in
1. **`n_signer` replaces the planned `identity-agent`.** `nsigner` already provides everything the design called for: mlock'd RAM-only key material, NIP-06 role derivation, NIP-46-style JSON-RPC signing methods, abstract-namespace Unix socket transport, zero-on-exit. We are not building a parallel daemon; we **delete** that work item.
2. **TTY1 is the signer terminal.** `nsigner` runs as a foreground program owning `/dev/tty1`. Mnemonic entry, status display, and approval prompts all live there.
3. **TTY2 is the post-unlock service-selection menu.** Trimmed [`stack/nostr-id-tui/`](../stack/nostr-id-tui/) on tty2 lets the user toggle service daemons.
4. **TTY3..TTY6 are normal login shells.** User logs in as the `nostr` user, runs `nt`, `otp`, and any other CLIs from those shells.
5. **No GUI in v1.** Virtual consoles + Ctrl+Alt+Fn give multi-terminal for free.
6. **`n_signer` v0.0.16+ `--preapprove` is the policy mechanism.** No separate policy file, no `--role-schedule` flag — preapproved caller/role pairs are passed as repeatable CLI flags on the `nsigner` ExecStart line.
7. **`@n_os_tr`** is the pinned abstract socket name. All consumers hard-code it.
8. **Service daemons get raw nsec at startup; user CLIs sign via NIP-46.** Two patterns coexist (see §7).
## 2. The components in scope
This integration covers **eight components** from the n-OS-tr ecosystem. They split into three categories by lifecycle:
| Category | Components | What they need from nsigner |
|---|---|---|
| **The signer itself** | [`n_signer`](../includes/n_signer/) | Holds the mnemonic; serves the rest |
| **Service daemons** (long-running, started by service menu) | [`c-relay`](../includes/c-relay/), [`ginxsom`](../includes/ginxsom/), [`fips`](../includes/fips/), `didactyl` | Raw nsec at startup (NIP-44 ECDH, Noise handshakes) |
| **User CLIs** (launched by user from shell) | `nt` (nostr_terminal), `otp`, future tools | NIP-46 `sign_event` over `@n_os_tr` |
| **Boot-time TUIs** (n-OS-tr-native) | [`nostr-id-tui`](../stack/nostr-id-tui/) (services menu) | NIP-46 `get_public_key(nostr_index=0)` to display the user's npub |
The role index assignment ([`plans/identity_subsystem.md` §2.1](identity_subsystem.md#21-the-role-table-schedule)) updated:
| Index | Role | Used by | Category |
|---|---|---|---|
| `0` | `main` | User's primary identity (`nt`, `otp`, future user tools) | User |
| `1` | `throwaway` | Test/experimental | User |
| `2` | `c-relay-admin` | c-relay daemon | Service |
| `3` | `ginxsom` | ginxsom daemon | Service |
| `4` | `fips` | fips mesh daemon | Service |
| `5` | `didactyl` | didactyl agent daemon | Service |
| `6+` | reserved/dynamic | future apps via registration | — |
## 3. Boot-time UX
```mermaid
sequenceDiagram
participant Kernel
participant Sd as systemd
participant Fb as n-os-tr-firstboot
participant TTY1 as nsigner @ tty1
participant TTY2 as nostr-id-tui @ tty2
participant TTY3 as getty @ tty3..6
participant Daemon as c-relay / ginxsom / fips / didactyl
participant CLI as nt / otp on user shell
Kernel->>Sd: hand off
Sd->>Fb: oneshot (no key generation)
Fb-->>Sd: done
Sd->>TTY1: start nsigner.service
with --preapprove flags
Sd->>TTY2: start nostr-id-tui-services.service
Sd->>TTY3: getty units (lazy-start on switch)
Note over TTY1: User sees: Enter mnemonic /
Generate new
Note over TTY2: User sees: Waiting for signer
polling @n_os_tr
TTY1->>TTY1: user types mnemonic
TTY1->>TTY1: derive role keys, bind @n_os_tr
load preapprove rules
TTY2->>TTY1: get_public_key nostr_index=0
(preapproved for uid:0)
TTY1-->>TTY2: npub1xyz...
TTY2->>TTY2: show service menu
TTY2->>Daemon: systemctl start
Daemon->>TTY1: get_secret_key nostr_index=N
(preapproved for the daemon's uid)
TTY1-->>Daemon: hex privkey
Daemon->>Daemon: hold key in mlock'd RAM,
do its job
Note over TTY3: user logs in on tty3
Note over CLI: user runs nt or otp
CLI->>TTY1: sign_event nostr_index=0
(preapproved for uid:1000)
TTY1-->>CLI: signed event
```
### 3.1 Concrete sequence
1. Boot completes. tty1 shows the `nsigner` mnemonic prompt. tty2 shows "waiting for signer" and starts polling `@n_os_tr`. tty3..tty6 are dormant.
2. User stays on tty1, types or generates the 12-word mnemonic. `nsigner` derives all role keys (indices 0–5 are pre-registered via the `--preapprove ...,nostr_index=N` startup flags), binds `@n_os_tr`, and the preapprove table is now active.
3. tty2's polling loop's first `get_public_key(nostr_index=0)` request hits — preapproved for `uid:0` (root) — returns the npub immediately, no prompt on tty1.
4. tty2 displays the npub for confirmation, then enters the service menu.
5. User toggles services on. `nostr-id-tui` does `systemctl start c-relay.service`. c-relay's `ExecStartPre` calls `n-os-tr-get-key c-relay-admin` which talks to `@n_os_tr`, asks for `get_secret_key(nostr_index=2)` — preapproved for the c-relay system user's uid — and exports the hex key into the service's environment.
6. Daemon runs. Same pattern for ginxsom, fips, didactyl.
7. User Ctrl+Alt+F3, logs in as `nostr` (uid 1000), runs `nt`. `nt` connects to `@n_os_tr`, calls `sign_event(nostr_index=0)`. Preapproved for `uid:1000`. Event is signed, no prompt on tty1.
### 3.2 Knowing which TTY you're on (visual indicators)
Linux gives you 6 virtual consoles for free, but stock Debian gives **no obvious indicator** of which one you're currently looking at. We address this with two layers, both essentially free:
**Layer 1 — Distinct banner per TTY.**
| TTY | Banner contents | Owner |
|---|---|---|
| tty1 | `🔐 N-OS-TR SIGNER · TTY1` in red, full-width | `nsigner` (via wrapper or upstream `--banner-prefix`) |
| tty2 | `⚙️ N-OS-TR SERVICES · TTY2` in blue, full-width | trimmed `nostr-id-tui` |
| tty3..tty6 | `n-OS-tr · SHELL · tty\l` baked into [`/etc/issue`](../iso/config/includes.chroot/etc/issue); shell `PS1=[tty\l \w]\$ ` | getty + `/etc/profile.d/n-os-tr-tty.sh` |
**Layer 2 — Background color tint per TTY.** Set via `setterm` in each unit's `ExecStartPre`.
| TTY | Tint |
|---|---|
| tty1 | dark red |
| tty2 | dark blue |
| tty3..tty6 | default |
The combination — color + banner + shell prompt — means at a glance the user can tell signer (red) from service menu (blue) from shell (black with `[ttyN]` prompt). Color-blind-friendly: shapes (🔐 vs ⚙️) and prompts also differ.
> **Open question (T9 in [`upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md)).** Adding "N-OS-TR SIGNER · TTY1" to nsigner's hardcoded banner needs upstream `--banner-prefix STRING` flag, OR a wrapper script, OR a build-time source patch. Lean: upstream.
### 3.3 Recovery / unhappy-path UX
- **Bad mnemonic.** `nsigner` clears its buffer and reprompts on tty1. tty2 keeps polling.
- **User quits `nsigner` (`q` hotkey).** Process exits, abstract socket disappears, key material is gone. systemd `Restart=always` brings up a fresh `nsigner` and the cycle starts over. tty2's polling loop catches the new instance; if the npub differs (different mnemonic), tty2 re-confirms before re-entering service menu.
- **A daemon crashes.** `nsigner` is unaffected. systemd's restart policy handles the daemon; on restart it re-fetches its key via `n-os-tr-get-key`.
- **A user CLI gets denied.** If the user runs `nt` from a uid not covered by a `--preapprove`, the first `sign_event` call returns `policy_denied`. The CLI surfaces "approval needed; check tty1" and the user walks to tty1, sees the prompt, presses `[a]` for session-grant.
## 4. nsigner.service — the core configuration
The service unit on tty1 that orchestrates everything. Replaces the existing [`nostr-id-tui.service`](../iso/config/includes.chroot/etc/systemd/system/nostr-id-tui.service).
```ini
[Unit]
Description=n-OS-tr signer (n_signer foreground)
Documentation=file:///usr/share/doc/n-os-tr/README
After=systemd-user-sessions.service n-os-tr-firstboot.service
Wants=n-os-tr-firstboot.service
Conflicts=getty@tty1.service
Before=getty@tty1.service
[Service]
Type=simple
ExecStartPre=/usr/bin/setterm -term linux -background red -store
ExecStart=/usr/local/sbin/nsigner \
--socket-name n_os_tr \
\
# tty2 service-menu polls for the user's npub. Root uid.
--preapprove caller=uid:0,nostr_index=0 \
\
# User CLIs (nt, otp) signing under the user identity.
# The 'nostr' user is uid 1000 in the live image.
--preapprove caller=uid:1000,nostr_index=0 \
\
# Service daemons. Each runs as its own dedicated system user.
--preapprove caller=uid:%U_C_RELAY%,nostr_index=2 \
--preapprove caller=uid:%U_GINXSOM%,nostr_index=3 \
--preapprove caller=uid:%U_FIPS%,nostr_index=4 \
--preapprove caller=uid:%U_DIDACTYL%,nostr_index=5
Restart=always
RestartSec=1
StandardInput=tty
StandardOutput=tty
StandardError=journal
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
# Memory hygiene (mirrors what n_signer enforces in-process)
LockPersonality=yes
NoNewPrivileges=yes
MemoryDenyWriteExecute=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
RestrictAddressFamilies=AF_UNIX
SystemCallArchitectures=native
CapabilityBoundingSet=CAP_IPC_LOCK
AmbientCapabilities=CAP_IPC_LOCK
[Install]
WantedBy=multi-user.target
```
**About the `%U_*%` placeholders.** systemd doesn't expand variables inside `ExecStart`. We have two options:
- **Option 4a (recommended): generate the unit at first boot** with the actual uids, after the system users are created. A small wrapper script reads `/etc/passwd`, looks up `c-relay`, `ginxsom`, etc., and writes the final unit file. Done once, at firstboot.
- **Option 4b:** Allocate fixed system uids in [`iso/config/hooks/live/0020-enable-services.hook.chroot`](../iso/config/hooks/live/0020-enable-services.hook.chroot) (e.g., `c-relay` is always uid 901, `ginxsom` always 902, etc.) and hard-code those in the unit. Simpler but less flexible.
Lean: **Option 4a**. The wrapper lives at [`iso/config/includes.chroot/usr/local/sbin/n-os-tr-render-nsigner-unit`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-render-nsigner-unit) and runs as part of `n-os-tr-firstboot`.
### 4.1 Why every preapprove uses `nostr_index=N`
We use `nostr_index=N` (not `role=`) for every preapprove rule. Reasons:
1. **Indices are canonical.** They're the actual NIP-06 derivation parameter; role names are just labels.
2. **No role-schedule file needed.** v0.0.16's `--preapprove ...,nostr_index=N` auto-creates the role at parse time. We get role registration for free as a side effect of the preapprove rules. **This closes upstream ticket T1.**
3. **The role-name list lives in [`plans/identity_subsystem.md` §2.1](identity_subsystem.md#21-the-role-table-schedule)** as the human-readable mapping. Code uses indices.
The auto-created role names from `nostr_index=N` are:
- index 0 → role `main`
- index N>0 → role `nostr_idx_N` (e.g., `nostr_idx_2`, `nostr_idx_5`)
So when c-relay calls `n-os-tr-get-key 2`, the helper translates that to `{"nostr_index": 2}` in the wire request, the response carries the key. The "c-relay-admin" name from our role schedule is purely documentation; it doesn't appear on the wire.
## 5. The key-fetch helper for service daemons
[`/usr/local/bin/n-os-tr-get-key`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-get-key) is the bridge between v0.0.16's RPC and a daemon's `ExecStartPre`. It already exists in the repo. Updated for v0.0.16 wire contract:
```bash
#!/bin/bash
# n-os-tr-get-key: request a role's secret key from nsigner via @n_os_tr
# Usage: n-os-tr-get-key
# Outputs: 64-char hex private key on stdout.
# Exit 1 on failure (signer down, denied, role not preapproved, ...).
set -euo pipefail
INDEX="${1:?usage: n-os-tr-get-key }"
SOCKET="n_os_tr"
TIMEOUT="${NSIGNER_KEY_TIMEOUT:-30}"
# Wait for socket. If user never unlocks signer, this times out and we fail closed.
ELAPSED=0
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
if socat -u OPEN:/dev/null ABSTRACT-CONNECT:"$SOCKET" 2>/dev/null; then
break
fi
sleep 1
ELAPSED=$((ELAPSED + 1))
done
[ "$ELAPSED" -ge "$TIMEOUT" ] && {
echo "n-os-tr-get-key: timeout waiting for @${SOCKET}" >&2; exit 1; }
# Build framed request (4-byte big-endian length + JSON payload)
PAYLOAD='{"id":"1","method":"get_secret_key","params":[{"nostr_index":'"$INDEX"'}]}'
PAYLOAD_LEN=${#PAYLOAD}
FRAME=$(printf '%08x' "$PAYLOAD_LEN" | xxd -r -p; printf '%s' "$PAYLOAD")
RESPONSE=$(printf '%s' "$FRAME" | socat -t5 - ABSTRACT-CONNECT:"$SOCKET" | tail -c +5)
# Check for error response
if echo "$RESPONSE" | grep -q '"error"'; then
ERR=$(echo "$RESPONSE" | grep -o '"message":"[^"]*"' | cut -d'"' -f4)
echo "n-os-tr-get-key: signer error: ${ERR:-unknown}" >&2; exit 1
fi
# Extract 64-char hex from result field
KEY=$(echo "$RESPONSE" | grep -oE '"result":"[a-f0-9]{64}"' | grep -oE '[a-f0-9]{64}' | head -1)
[ -z "$KEY" ] || [ "${#KEY}" -ne 64 ] && {
echo "n-os-tr-get-key: invalid response" >&2; exit 1; }
echo "$KEY"
```
> **Critical caveat.** `get_secret_key` is **not yet implemented in upstream `n_signer`** as of v0.0.16. This is open ticket T3 in [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md). Until it lands, the helper above will receive `method_not_found` and the daemons will fail closed. See §9 for the path forward.
## 6. Per-daemon systemd drop-ins
Each service daemon gets a drop-in that adds the signer-aware behavior. These already exist for c-relay, ginxsom, fips, nginx; we add didactyl.
### 6.1 c-relay
[`/etc/systemd/system/c-relay.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/c-relay.service.d/n-os-tr-signer.conf):
```ini
[Unit]
After=nsigner.service
Wants=nsigner.service
[Service]
Environment=NSIGNER_SOCKET=@n_os_tr
ExecStartPre=/usr/local/bin/n-os-tr-await-signer
# Get the relay-admin key, export to env. The key never touches disk.
ExecStartPre=/bin/bash -c 'echo "C_RELAY_ADMIN_PRIVKEY=$(/usr/local/bin/n-os-tr-get-key 2)" > /run/c-relay.env && chmod 0400 /run/c-relay.env'
EnvironmentFile=/run/c-relay.env
ExecStartPost=/bin/rm -f /run/c-relay.env
```
The transient `/run/c-relay.env` exists for milliseconds between `ExecStartPre` and `ExecStartPost`. systemd reads `EnvironmentFile=` between the two.
### 6.2 ginxsom
Same pattern, `nostr_index=3`. Ginxsom takes `--server-privkey` as a CLI arg, so the drop-in also rewrites the `ExecStart`:
[`/etc/systemd/system/ginxsom.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/ginxsom.service.d/n-os-tr-signer.conf):
```ini
[Unit]
After=nsigner.service
Wants=nsigner.service
[Service]
Environment=NSIGNER_SOCKET=@n_os_tr
ExecStartPre=/usr/local/bin/n-os-tr-await-signer
ExecStartPre=/bin/bash -c 'echo "GINXSOM_SERVER_PRIVKEY=$(/usr/local/bin/n-os-tr-get-key 3)" > /run/ginxsom.env && chmod 0400 /run/ginxsom.env'
EnvironmentFile=/run/ginxsom.env
# Inherit the upstream ExecStart but add the privkey as an env-substituted arg
ExecStart=
ExecStart=/usr/local/bin/ginxsom/ginxsom-fcgi --server-privkey ${GINXSOM_SERVER_PRIVKEY}
ExecStartPost=/bin/rm -f /run/ginxsom.env
```
### 6.3 fips
`nostr_index=4`. fips reads its identity from a config field. We render the YAML at startup:
[`/etc/systemd/system/fips.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/fips.service.d/n-os-tr-signer.conf):
```ini
[Unit]
After=nsigner.service
Wants=nsigner.service
[Service]
Environment=NSIGNER_SOCKET=@n_os_tr
ExecStartPre=/usr/local/bin/n-os-tr-await-signer
# Render fips config with the live nsec from nsigner. /run is tmpfs.
ExecStartPre=/bin/bash -c '\
KEY=$(/usr/local/bin/n-os-tr-get-key 4) && \
install -d -m 0700 /run/fips && \
cp /etc/fips/fips.yaml /run/fips/fips.yaml && \
echo "node:" >> /run/fips/fips.yaml && \
echo " identity:" >> /run/fips/fips.yaml && \
echo " nsec: $KEY" >> /run/fips/fips.yaml && \
chmod 0400 /run/fips/fips.yaml'
ExecStart=
ExecStart=/usr/local/bin/fips --config /run/fips/fips.yaml
```
### 6.4 didactyl (new)
`nostr_index=5`. Didactyl reads its key from a `genesis.jsonc` config file:
[`/etc/systemd/system/didactyl.service`](../iso/config/includes.chroot/etc/systemd/system/didactyl.service):
```ini
[Unit]
Description=Didactyl Nostr agent
After=nsigner.service network-online.target
Wants=nsigner.service network-online.target
[Service]
Type=simple
User=didactyl
Group=didactyl
Environment=NSIGNER_SOCKET=@n_os_tr
ExecStartPre=/usr/local/bin/n-os-tr-await-signer
ExecStartPre=/bin/bash -c '\
KEY_HEX=$(/usr/local/bin/n-os-tr-get-key 5) && \
install -d -m 0700 -o didactyl -g didactyl /run/didactyl && \
/usr/local/bin/n-os-tr-render-didactyl-genesis "$KEY_HEX" > /run/didactyl/genesis.jsonc && \
chmod 0400 /run/didactyl/genesis.jsonc && \
chown didactyl:didactyl /run/didactyl/genesis.jsonc'
ExecStart=/usr/local/bin/didactyl_static_x86_64 --config /run/didactyl/genesis.jsonc
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
The render helper [`n-os-tr-render-didactyl-genesis`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-render-didactyl-genesis) takes the hex key, derives npub/nsec from it (using `nak` or a small helper), and emits the JSON didactyl expects (see [`didactyl/README.md`](../../didactyl/README.md) for schema).
### 6.5 nginx
No change from current — already gated by `n-os-tr-await-signer` so it doesn't start before identity is loaded.
## 7. User CLIs: `nt`, `otp`, future tools
User CLIs are different from service daemons:
- They're launched by the **user**, from a shell, on tty3+
- They run as the user's uid (e.g., `uid:1000` for the `nostr` user)
- They want to sign Nostr events, not run forever
- They should NOT need raw nsec — they should use NIP-46 `sign_event` cleanly
### 7.1 Configuration: tell each CLI where the signer is
Both `nt` and `otp` (and future user CLIs) need a way to discover the signer. We standardize on:
- **Environment variable** `NOSTR_SIGNER_SOCKET=@n_os_tr` exported in the user's shell (set in [`/etc/profile.d/n-os-tr-tty.sh`](../iso/config/includes.chroot/etc/profile.d/n-os-tr-tty.sh)).
- **Login flow:** when `nt` starts and finds `NOSTR_SIGNER_SOCKET` set, it skips its built-in mnemonic prompt and uses the signer instead. (This is a per-CLI feature that needs implementing — see §9.)
### 7.2 Preapprove for the user's uid
The `nsigner.service` unit's preapprove flags include `caller=uid:1000,nostr_index=0`, granting the user (uid 1000) auto-allow access to sign_event for role `main`. So:
```bash
# On tty3, logged in as nostr user:
$ nt
[nt] using signer @n_os_tr (uid:1000 → nostr_index=0)
[nt] hello, npub1xyz...
> publish "hello world"
[nt] signed via nsigner, published to relays
```
No prompt fires on tty1; the request is preapproved.
If the user wants more granular control (e.g., always-prompt for `otp` because the messages are sensitive), they can either:
- Remove the `caller=uid:1000,nostr_index=0` preapprove flag from `nsigner.service` (every CLI signing requires tty1 approval), or
- Run with a different uid for the sensitive CLI (run `otp` as `uid:1001`, no preapprove for that uid)
### 7.3 What needs to change in `nt` and `otp`
Both currently expect the user to enter a mnemonic/nsec in their own UI. To use n_signer instead, each CLI needs to:
1. Detect `NOSTR_SIGNER_SOCKET` env var and a `--use-signer` flag (or treat env-var presence as opt-in)
2. Implement a NIP-46 over abstract-Unix-socket client (the same JSON-RPC framing nsigner uses)
3. Replace local mnemonic/nsec storage with calls to `get_public_key` and `sign_event`
4. Fall back to local mnemonic if signer isn't present (preserves existing behavior outside n-OS-tr)
This is **upstream work in `nt` and `otp`**, not in this repo. We file tickets against those projects:
> **Open question (T11).** File feature requests in `nostr_terminal` and `otp` for "signer integration" — accept `NOSTR_SIGNER_SOCKET` env var, speak nsigner's framed JSON-RPC, fall back gracefully if absent. Track outside this plan.
## 8. The n-os-tr service menu (tty2)
[`stack/nostr-id-tui/`](../stack/nostr-id-tui/) trimmed down to just the post-unlock service menu. Polls `@n_os_tr` for the main npub, then lets the user toggle service daemons on/off.
The service list (in [`/etc/n-os-tr/services.conf`](../iso/config/includes.chroot/etc/n-os-tr/services.conf)) now includes `didactyl`:
```
# /etc/n-os-tr/services.conf — services toggleable from tty2
c-relay:c-relay.service:Nostr relay (your own NIP-01 relay)
ginxsom:ginxsom.service:Blossom blob server (BUD-* protocols)
fips:fips.service:FIPS mesh routing (npub-addressed IPv6)
didactyl:didactyl.service:Didactyl AI agent
nginx:nginx.service:HTTPS / reverse proxy
```
(Format: `display-name:unit-name:description`.)
## 9. Required upstream work
To make the integration fully functional we still need the following, tracked in [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md):
| Ticket | Status | What | Blocks |
|---|---|---|---|
| T1: `--role-schedule FILE` | **Closed** | Resolved by `--preapprove ...,nostr_index=N` auto-creating roles | nothing |
| T2: `--policy-file PATH` | **Closed** | Resolved by `--preapprove` CLI flag | nothing |
| T3: `get_secret_key` method | **Open** | RPC method to expose raw nsec for a role index, gated by policy | All four service daemons |
| T9: `--banner-prefix STRING` | **Open**, low-priority | Inject "N-OS-TR SIGNER · TTY1" into nsigner banner | UX nice-to-have |
| T11: `nt` and `otp` signer integration | **Open** | Upstream changes in those projects to use NIP-46 over `@n_os_tr` | User CLI signing |
T3 is the critical-path blocker. Until it lands, we can ship the ISO with:
- nsigner working (mnemonic entry, status display, prompts)
- tty2 service menu working (uses `get_public_key` which exists)
- User CLIs working **if `nt`/`otp` are updated** to use `sign_event` (T11)
- Service daemons **broken** — they can't get raw keys
Mitigation while T3 is pending: keep [`n-os-tr-firstboot`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot)'s old key-generation path as a feature-flag fallback. If `n-os-tr-get-key` returns `method_not_found`, the daemon's `ExecStartPre` falls back to the old behavior (random nsec generated at firstboot, stored in `/var/lib/n-os-tr/keys/`). This is **insecure** by our threat model — the keys touch disk — but it lets the rest of the system work for testing while we land T3 upstream.
## 10. Threat-model implications
Strengthens [`plans/threat_model.md`](threat_model.md):
- **F1 (no on-disk mnemonic):** preserved. nsigner mlocks; mnemonic never persisted.
- **F2 (no swap):** preserved. nsigner calls mlock; image disables swap kernel-wide.
- **F4 (no key material in journald):** preserved. nsigner doesn't log keys; the transient `/run/.env` files are short-lived tmpfs.
- **F12 (no detached daemon for keys):** preserved. nsigner is foreground, attached to tty1, dies cleanly.
New attack surfaces:
- **The `@n_os_tr` socket itself.** Gated by `SO_PEERCRED`; preapprove rules pin specific uids. Anyone running outside the preapprove list still hits the prompt path on tty1.
- **The `--preapprove` CLI args.** Visible in `ps`/`/proc`. Contents are public (uid + role index, no secrets), so this is benign.
- **`/run/.env` transient files.** Exist for milliseconds during ExecStartPre→ExecStart→ExecStartPost. On tmpfs (RAM, no swap). Mode 0400. Cleared by ExecStartPost. Acceptable.
## 11. Documents that need updating after this lands
- [`plans/identity_subsystem.md`](identity_subsystem.md) — already has deprecation banner. Update §2.1 to add didactyl at index 5.
- [`plans/iso_architecture.md`](iso_architecture.md) — boot diagram updated to show didactyl alongside the other daemons.
- [`plans/binary_handling_strategy.md`](binary_handling_strategy.md) — add didactyl, nostr_terminal, otp to the Tier 1 component list.
- [`plans/N_OS_TR_AGENT_CHANGES.md`](N_OS_TR_AGENT_CHANGES.md) — already up-to-date for v0.0.16; this plan integrates with it.
- [`README.md`](../README.md) — add `nt`, `otp`, `didactyl` to the components list; mention multi-VT briefly.
- [`docs/RUNNING.md`](../docs/RUNNING.md) — already has VT switching section; add CLI usage examples for `nt` and `otp`.
## 12. Summary diagram
```mermaid
flowchart TB
subgraph TTYs[Linux virtual consoles]
T1[tty1 RED: nsigner
mnemonic + status + prompts]
T2[tty2 BLUE: nostr-id-tui
service menu]
T3[tty3..tty6: shell
uid:1000 nostr user]
end
Sock[@n_os_tr
abstract Unix socket, pinned name
--preapprove rules loaded at startup]
subgraph Daemons[Service daemons - long-running]
Cr[c-relay
uid:c-relay
nostr_index=2]
Gx[ginxsom
uid:ginxsom
nostr_index=3]
Fp[fips
uid:fips
nostr_index=4]
Dd[didactyl
uid:didactyl
nostr_index=5]
end
subgraph CLIs[User CLIs - launched from shell]
Nt[nt
nostr terminal]
Otp[otp
one-time pad]
end
T1 -- binds --> Sock
T2 -- get_public_key nostr_index=0
caller=uid:0 preapproved --> Sock
Cr -- get_secret_key nostr_index=2
caller=uid:c-relay preapproved --> Sock
Gx -- get_secret_key nostr_index=3 --> Sock
Fp -- get_secret_key nostr_index=4 --> Sock
Dd -- get_secret_key nostr_index=5 --> Sock
Nt -- sign_event nostr_index=0
caller=uid:1000 preapproved --> Sock
Otp -- sign_event nostr_index=0 --> Sock
Sock -.prompt for unknown callers.-> T1
```