26 KiB
Integrating n_signer into n-OS-tr
Status: Design (revised for
n_signerv0.0.16--preapprovemodel). Defines how the vendoredincludes/n_signer/program becomes the boot-time mnemonic-entry + signer for n-OS-tr, replacing the previously-plannedidentity-agentdaemon, and how the user gets a working multi-terminal experience without going GUI.Supersedes the daemon design in
plans/identity_subsystem.md. The mnemonic-entry portion ofplans/tui_login.mdis also superseded; the post-unlock service menu part of that doc remains valid (now hosted bystack/nostr-id-tui/on tty2).Companion docs:
plans/N_OS_TR_AGENT_CHANGES.md(operational guide for the v0.0.16 approval model),plans/binary_handling_strategy.md(how all binaries get into the ISO),plans/upstream_nsigner_tickets.md(open upstream features still needed).
1. Decisions locked in
n_signerreplaces the plannedidentity-agent.nsigneralready 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.- TTY1 is the signer terminal.
nsignerruns as a foreground program owning/dev/tty1. Mnemonic entry, status display, and approval prompts all live there. - TTY2 is the post-unlock service-selection menu. Trimmed
stack/nostr-id-tui/on tty2 lets the user toggle service daemons. - TTY3..TTY6 are normal login shells. User logs in as the
nostruser, runsnt,otp, and any other CLIs from those shells. - No GUI in v1. Virtual consoles + Ctrl+Alt+Fn give multi-terminal for free.
n_signerv0.0.16+--preapproveis the policy mechanism. No separate policy file, no--role-scheduleflag — preapproved caller/role pairs are passed as repeatable CLI flags on thensignerExecStart line.@n_os_tris the pinned abstract socket name. All consumers hard-code it.- 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 |
Holds the mnemonic; serves the rest |
| Service daemons (long-running, started by service menu) | c-relay, ginxsom, 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 (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) 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
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<br/>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 /<br/>Generate new
Note over TTY2: User sees: Waiting for signer<br/>polling @n_os_tr
TTY1->>TTY1: user types mnemonic
TTY1->>TTY1: derive role keys, bind @n_os_tr<br/>load preapprove rules
TTY2->>TTY1: get_public_key nostr_index=0<br/>(preapproved for uid:0)
TTY1-->>TTY2: npub1xyz...
TTY2->>TTY2: show service menu
TTY2->>Daemon: systemctl start <daemon>
Daemon->>TTY1: get_secret_key nostr_index=N<br/>(preapproved for the daemon's uid)
TTY1-->>Daemon: hex privkey
Daemon->>Daemon: hold key in mlock'd RAM,<br/>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<br/>(preapproved for uid:1000)
TTY1-->>CLI: signed event
3.1 Concrete sequence
- Boot completes. tty1 shows the
nsignermnemonic prompt. tty2 shows "waiting for signer" and starts polling@n_os_tr. tty3..tty6 are dormant. - User stays on tty1, types or generates the 12-word mnemonic.
nsignerderives all role keys (indices 0–5 are pre-registered via the--preapprove ...,nostr_index=Nstartup flags), binds@n_os_tr, and the preapprove table is now active. - tty2's polling loop's first
get_public_key(nostr_index=0)request hits — preapproved foruid:0(root) — returns the npub immediately, no prompt on tty1. - tty2 displays the npub for confirmation, then enters the service menu.
- User toggles services on.
nostr-id-tuidoessystemctl start c-relay.service. c-relay'sExecStartPrecallsn-os-tr-get-key c-relay-adminwhich talks to@n_os_tr, asks forget_secret_key(nostr_index=2)— preapproved for the c-relay system user's uid — and exports the hex key into the service's environment. - Daemon runs. Same pattern for ginxsom, fips, didactyl.
- User Ctrl+Alt+F3, logs in as
nostr(uid 1000), runsnt.ntconnects to@n_os_tr, callssign_event(nostr_index=0). Preapproved foruid: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; 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). Adding "N-OS-TR SIGNER · TTY1" to nsigner's hardcoded banner needs upstream--banner-prefix STRINGflag, OR a wrapper script, OR a build-time source patch. Lean: upstream.
3.3 Recovery / unhappy-path UX
- Bad mnemonic.
nsignerclears its buffer and reprompts on tty1. tty2 keeps polling. - User quits
nsigner(qhotkey). Process exits, abstract socket disappears, key material is gone. systemdRestart=alwaysbrings up a freshnsignerand 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.
nsigneris unaffected. systemd's restart policy handles the daemon; on restart it re-fetches its key vian-os-tr-get-key. - A user CLI gets denied. If the user runs
ntfrom a uid not covered by a--preapprove, the firstsign_eventcall returnspolicy_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.
[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 upc-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(e.g.,c-relayis always uid 901,ginxsomalways 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 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=<name>) for every preapprove rule. Reasons:
- Indices are canonical. They're the actual NIP-06 derivation parameter; role names are just labels.
- No role-schedule file needed. v0.0.16's
--preapprove ...,nostr_index=Nauto-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. - The role-name list lives in
plans/identity_subsystem.md§2.1 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 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:
#!/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 <nostr_index>
# 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 <nostr_index>}"
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_keyis not yet implemented in upstreamn_signeras of v0.0.16. This is open ticket T3 inplans/upstream_nsigner_tickets.md. Until it lands, the helper above will receivemethod_not_foundand 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:
[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:
[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:
[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:
[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 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 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:1000for thenostruser) - They want to sign Nostr events, not run forever
- They should NOT need raw nsec — they should use NIP-46
sign_eventcleanly
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_trexported in the user's shell (set in/etc/profile.d/n-os-tr-tty.sh). - Login flow: when
ntstarts and findsNOSTR_SIGNER_SOCKETset, 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:
# 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=0preapprove flag fromnsigner.service(every CLI signing requires tty1 approval), or - Run with a different uid for the sensitive CLI (run
otpasuid: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:
- Detect
NOSTR_SIGNER_SOCKETenv var and a--use-signerflag (or treat env-var presence as opt-in) - Implement a NIP-46 over abstract-Unix-socket client (the same JSON-RPC framing nsigner uses)
- Replace local mnemonic/nsec storage with calls to
get_public_keyandsign_event - 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_terminalandotpfor "signer integration" — acceptNOSTR_SIGNER_SOCKETenv 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/ 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) 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:
| 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_keywhich exists) - User CLIs working if
nt/otpare updated to usesign_event(T11) - Service daemons broken — they can't get raw keys
Mitigation while T3 is pending: keep 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:
- 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/<service>.envfiles 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_trsocket itself. Gated bySO_PEERCRED; preapprove rules pin specific uids. Anyone running outside the preapprove list still hits the prompt path on tty1. - The
--preapproveCLI args. Visible inps//proc. Contents are public (uid + role index, no secrets), so this is benign. /run/<service>.envtransient 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— already has deprecation banner. Update §2.1 to add didactyl at index 5.plans/iso_architecture.md— boot diagram updated to show didactyl alongside the other daemons.plans/binary_handling_strategy.md— add didactyl, nostr_terminal, otp to the Tier 1 component list.plans/N_OS_TR_AGENT_CHANGES.md— already up-to-date for v0.0.16; this plan integrates with it.README.md— addnt,otp,didactylto the components list; mention multi-VT briefly.docs/RUNNING.md— already has VT switching section; add CLI usage examples forntandotp.
12. Summary diagram
flowchart TB
subgraph TTYs[Linux virtual consoles]
T1[tty1 RED: nsigner<br/>mnemonic + status + prompts]
T2[tty2 BLUE: nostr-id-tui<br/>service menu]
T3[tty3..tty6: shell<br/>uid:1000 nostr user]
end
Sock[@n_os_tr<br/>abstract Unix socket, pinned name<br/>--preapprove rules loaded at startup]
subgraph Daemons[Service daemons - long-running]
Cr[c-relay<br/>uid:c-relay<br/>nostr_index=2]
Gx[ginxsom<br/>uid:ginxsom<br/>nostr_index=3]
Fp[fips<br/>uid:fips<br/>nostr_index=4]
Dd[didactyl<br/>uid:didactyl<br/>nostr_index=5]
end
subgraph CLIs[User CLIs - launched from shell]
Nt[nt<br/>nostr terminal]
Otp[otp<br/>one-time pad]
end
T1 -- binds --> Sock
T2 -- get_public_key nostr_index=0<br/>caller=uid:0 preapproved --> Sock
Cr -- get_secret_key nostr_index=2<br/>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<br/>caller=uid:1000 preapproved --> Sock
Otp -- sign_event nostr_index=0 --> Sock
Sock -.prompt for unknown callers.-> T1