# Full n_signer Transport Support in the Setup Wizard

## Context

`n_signer` (upgraded to v0.1.5) now exposes four transports and a new
`nostr_mine_event` verb. `nostr_core_lib` (v0.6.6, the client lib didactyl links
against) already exposes all four transport factories and discovery helpers, but
didactyl's interactive setup wizard and config/main wiring only recognize
`nsigner_unix` and `nsigner_tcp`. The existing-agent recovery path still
requires an nsec and uses raw-key NIP-44, which defeats the point of remote
signer modes.

This plan closes those gaps so an operator can stand up a new agent or recover
an existing one using any n_signer transport, with the nsec never entering the
agent process.

## Current State (verified)

- `nostr_core_lib/nostr_core/nostr_signer.h` exposes:
  - `nostr_signer_nsigner_unix`, `nostr_signer_nsigner_tcp`,
    `nostr_signer_nsigner_serial`, `nostr_signer_nsigner_fds`
  - `nostr_signer_nsigner_set_auth` (TCP auth envelope)
- `nostr_core_lib/nostr_core/nsigner_transport.h` exposes:
  - `nsigner_transport_list_unix` (enumerate `@nsigner*` abstract sockets)
  - `nsigner_transport_list_serial` (enumerate `/dev/ttyACM*`)
- `nostr_core_lib/nostr_core/nostr_signer.c` routes the four core verbs
  (get_public_key, sign_event, nip04/nip44 encrypt/decrypt) through
  `nostr_signer_t` for all backends. **`nostr_mine_event` is NOT wired.**
- `src/config.h` `signer_config_t` has fields for `mode`, `socket_name`,
  `role`, `timeout_ms`, `auth_privkey_hex`, `tcp_host`, `tcp_port`,
  `emergency_local_nsec`. **No serial device or fds fields.**
- `src/config.c` `signer_mode_is_known()` / `signer_mode_is_remote()` only
  accept `local`, `nsigner_unix`, `nsigner_tcp`.
- `src/main.c` `apply_signer_overrides()` and `construct_signer()` only handle
  `local`, `nsigner_unix`, `nsigner_tcp`.
- `src/setup_wizard.c` `new_agent_identity_step()` option `s` hardcodes
  `nsigner_unix`, prompts for socket/role/timeout, does a connectivity check,
  and offers no auto-discovery or TCP/serial/fds choice.
- `src/setup_wizard.c` `existing_agent_flow()` requires an nsec and uses
  `nostr_nip44_decrypt`/`nostr_nip44_encrypt` with the raw private key for
  kind-30078 config recovery (Phase 3 item 13 DEFERRED notes).
- `src/setup_wizard.c` `install_system_service_with_dedicated_user()` builds
  `key_args` for `nsigner_unix` and `nsigner_tcp` only.

## Architecture

```mermaid
flowchart TD
    A[Wizard Main Menu] --> B{New or Existing?}
    B -->|New| C[Identity Step]
    B -->|Existing| D[Signer-or-nsec Step]
    C --> E[Signer Transport Picker]
    D --> E
    E --> F{Transport}
    F -->|Unix| G[List @nsigner sockets via nsigner_transport_list_unix]
    F -->|TCP| H[Prompt host:port + optional auth privkey]
    F -->|Serial| I[List /dev/ttyACM* via nsigner_transport_list_serial]
    F -->|FDs| J[Prompt read_fd/write_fd]
    F -->|Local nsec| K[Legacy raw-key path]
    G --> L[Connectivity check via nostr_signer_get_public_key]
    H --> L
    I --> L
    J --> L
    K --> M[Derive keys from nsec]
    L --> N[Populate cfg.signer + cfg.keys.public_key_hex]
    M --> N
    N --> O[Continue wizard flow]
```

## Changes by File

### 1. `src/config.h` — extend `signer_config_t`

- Update the `mode` comment to list all five modes:
  `local | nsigner_unix | nsigner_tcp | nsigner_serial | nsigner_fds`.
- Add fields:
  - `char serial_device[OW_MAX_URL_LEN]` — path for `nsigner_serial`.
  - `int fds_read_fd`, `int fds_write_fd` — for `nsigner_fds` (CLI-only; not
    persisted to genesis since fds are runtime-only).
- Add a new `OW_MAX_SIGNER_DEVICE_LEN` constant (reuse `OW_MAX_URL_LEN`).

### 2. `src/config.c` — accept the new modes

- `signer_mode_is_known()`: add `nsigner_serial`, `nsigner_fds`.
- `signer_mode_is_remote()`: add `nsigner_serial`, `nsigner_fds`.
- `parse_signer_config()`: parse `serial_device` string. Do NOT parse
  `fds_read_fd`/`fds_write_fd` from JSON (they are CLI-only); document this.
- Update the error message string to list all five modes.
- `config_set_defaults()` (signer block): leave `mode="local"`, zero the new
  fields.

### 3. `src/main.c` — construct signer for all transports

- `signer_mode_is_remote_local()`: add `nsigner_serial`, `nsigner_fds`.
- `apply_signer_overrides()`:
  - Add `--signer-serial <device>` CLI flag → sets mode `nsigner_serial` and
    `cfg->signer.serial_device`.
  - Add `--signer-fds <read_fd:write_fd>` CLI flag → sets mode `nsigner_fds`
    and parses the two fds.
  - Update the validation block and error message to list all five modes.
- `construct_signer()`:
  - Add `nsigner_serial` branch → `nostr_signer_nsigner_serial(
    cfg->signer.serial_device, role, timeout_ms)`.
  - Add `nsigner_fds` branch → `nostr_signer_nsigner_fds(
    cfg->signer.fds_read_fd, cfg->signer.fds_write_fd, role, timeout_ms)`.
  - Update the "Unknown signer mode" fallback message.
  - The existing connectivity-check + pubkey-discovery block already works for
    all remote backends (it calls `nostr_signer_get_public_key`), so no change
    needed there.
- Update `print_usage()` help text to document `--signer-serial` and
  `--signer-fds`.

### 4. `src/setup_wizard.c` — new signer transport picker

Replace the hardcoded `nsigner_unix` block in `new_agent_identity_step()`
option `s` with a new helper `prompt_signer_transport(cfg)` that:

1. Renders a sub-menu:
   - `u` nix socket (auto-discover or named)
   - `t` cp (host:port + optional auth privkey)
   - `s` erial USB device (auto-discover or path)
   - `f` d pair (read_fd:write_fd — advanced)
   - `b` ack
2. For `u`: call `nsigner_transport_list_unix()` to enumerate running n_signer
   abstract sockets; if any are found, list them numbered and let the operator
   pick one or type a custom name; if none found, prompt for a name (empty =
   auto-discover at boot).
3. For `t`: prompt `host:port` and an optional 64-char hex auth privkey
   (echo-suppressed). Store into `cfg->signer.tcp_host/tcp_port/
   auth_privkey_hex`.
4. For `s`: call `nsigner_transport_list_serial()` to enumerate
   `/dev/ttyACM*`; list them and let the operator pick or type a custom path.
   Store into `cfg->signer.serial_device`.
5. For `f`: prompt `read_fd:write_fd` (advanced; mainly for qrexec-style
   deployments). Store into `cfg->signer.fds_read_fd/fds_write_fd`.
6. Common: prompt `role` (default `main`) and `timeout_ms` (default `15000`).
7. Set `cfg->signer.mode` to the chosen mode.
8. Connectivity check: construct an ephemeral `nostr_signer_t*` via the
   matching factory, call `nostr_signer_get_public_key()` to populate
   `cfg->keys.public_key_hex`, then `nostr_signer_free()`. On failure, print
   the error and loop back to the transport picker.
9. Guard all nsigner_* branches with `#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)`
   and emit the existing "compiled without n_signer client support" warning
   otherwise.

Add a parallel entry point to `existing_agent_flow()` so the operator can
choose "recover via a running n_signer" instead of entering an nsec. See item 5.

### 5. `src/setup_wizard.c` — existing-agent recovery via signer verbs

Refactor `existing_agent_flow()`:

- New first step: choose identity source:
  - `n` sec (legacy)
  - `s` ign with a running n_signer (calls `prompt_signer_transport(cfg)`)
- When the signer path is chosen:
  - The connectivity check populates `cfg->keys.public_key_hex` from
    `nostr_signer_get_public_key()`.
  - Keep the ephemeral signer handle alive for the recovery queries instead
    of freeing it immediately (pass it through to the recovery helpers).
- Replace the raw-key NIP-44 decrypt in `fetch_and_decrypt_self_config_wizard()`
  with a signer-aware variant: when a signer is available, call
  `nostr_signer_nip44_decrypt(signer, cfg->keys.public_key_hex, ciphertext,
  &plaintext)`. Fall back to the raw-key path only when `signer == NULL`
  (local mode). The peer pubkey for self-encrypt is the agent's own pubkey.
- Replace the raw-key NIP-44 encrypt in
  `publish_encrypted_self_config_wizard()` the same way.
- Remove/replace the Phase 3 item 13 DEFERRED comments at those two sites
  since the signer path is now implemented for the wizard.
- The `nostr_handler_init(&tmp)` calls in the recovery helpers currently copy
  the config and re-derive keys; ensure they pick up `cfg->signer` so the
  handler uses the process signer for its own encrypt/decrypt. (Verify
  `nostr_handler` already threads the signer through; if not, pass the
  ephemeral wizard signer via a new `nostr_handler_set_signer()` call for the
  duration of the recovery queries, then clear it before `cleanup()`.)

### 6. `src/setup_wizard.c` — systemd unit key_args for new modes

In `install_system_service_with_dedicated_user()`, extend the `key_args`
builder:
- `nsigner_serial`: `--signer nsigner_serial --signer-serial <device>
  --signer-role <role> --signer-timeout <ms>`
- `nsigner_fds`: skip systemd install (fds are runtime-only and cannot be
  embedded in ExecStart). Print a clear message that fds mode is not
  installable as a systemd service and the operator must wire the fd-passing
  themselves (e.g. via a wrapper unit with `Sockets=` or qrexec). Return
  non-fatal error so the wizard offers "boot now" instead.
- Update the summary print block (`Signer: %s ...`) to mention serial/fds.

### 7. `src/main.c` — `--signer-serial` / `--signer-fds` arg parsing

Already covered in item 3. Ensure `argv` parsing loop handles the two new
flags and their values, and that `--signer-fds` parses `read:write` with
`atoi` validation (both >= 0).

### 8. `nostr_core_lib` — wire `nostr_mine_event` (optional, stretch)

This is a library-side change. If we want didactyl to be able to mine PoW
events through a remote signer:
- Add `int nostr_signer_mine_event(nostr_signer_t*, const cJSON* unsigned_event,
  int difficulty, int threads, int timeout_sec, cJSON** signed_event_out)` to
  `nostr_signer.h`.
- Implement the remote branch in `nostr_signer.c` calling
  `nsigner_client_call(client, "nostr_mine_event", params, &result)` with the
  options object `{difficulty, threads, timeout_sec}`.
- Local branch: call the existing `nostr_mine_event` helper in `nip013.c`.
- This is **not required** for the wizard task; include only if the operator
  wants PoW through n_signer. Recommend deferring to a follow-up.

### 9. Docs

- Update `docs/GENESIS.md` signer block example to show all five modes and the
  new `serial_device` field.
- Update `README.md` quick-start example #5 to mention TCP/serial variants.
- Add a short section to `docs/CONTEXT.md` (or a new `docs/SIGNER.md`)
  describing the five modes, when to use each, and the security tradeoff
  (local keeps nsec in-process; remote modes keep it in n_signer).

## Verification

1. `make deps && make` — must compile cleanly with
   `-DNOSTR_ENABLE_NSIGNER_CLIENT=1`.
2. Run `./didactyl --help` — confirm `--signer-serial` and `--signer-fds`
   appear.
3. Start a local n_signer on an abstract unix socket; run `./didactyl`
   (interactive), choose New agent → sign with n_signer → unix → pick the
   discovered socket → confirm connectivity check prints the pubkey.
4. Repeat with `--signer-tcp 127.0.0.1:11111` style flow via the wizard TCP
   option.
5. Existing-agent flow: with a running n_signer, choose Existing → sign with
   n_signer → confirm kind-30078 config is decrypted via the signer (no nsec
   entered). Verify the recovered LLM/admin fields appear.
6. Negative: kill n_signer mid-wizard → connectivity check fails → wizard
   loops back to the transport picker.
7. `nsigner_serial` / `nsigner_fds` modes: validate config load via a
   genesis file with `signer.mode: "nsigner_serial"` and
   `serial_device: "/dev/ttyACM0"`; confirm `construct_signer()` attempts the
   serial factory (will fail without hardware, but the mode must be accepted
   and reach the factory, not be rejected as "unknown signer mode").

## Out of Scope

- Wiring `nostr_mine_event` into didactyl call sites (only the lib hook in
  item 8, and only if explicitly requested).
- Changing the `nostr_handler` signer threading model beyond what the wizard
  recovery path needs.
- Windows support for abstract sockets / serial enumeration (Linux-only, as
  today).
