v0.2.56 - nostr_core_lib v0.6.10→v0.6.15: replace nostr_index with role_path, add signer_crypto tool, nostr_post PoW, nostr_signer_last_error surfacing. Wizard: toggle-based relay menu, admin keypair generation, reordered steps, existing-agent download/review, default role nostr_range, API key visible input
This commit is contained in:
@@ -35,6 +35,7 @@ SRCS = \
|
||||
$(SRC_DIR)/tools/tool_config.c \
|
||||
$(SRC_DIR)/tools/tool_cashu_wallet.c \
|
||||
$(SRC_DIR)/tools/tool_blossom.c \
|
||||
$(SRC_DIR)/tools/tool_signer_crypto.c \
|
||||
$(SRC_DIR)/trigger_manager.c \
|
||||
$(SRC_DIR)/prompt_template.c \
|
||||
$(SRC_DIR)/http_api.c \
|
||||
|
||||
@@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.2.55
|
||||
## Current Status — v0.2.56
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.2.55 — Fix wizard quit bug, add nostr_index selector, fix relay flapping backoff defeat, add NOSTR_ENABLE_NSIGNER_CLIENT to Dockerfile
|
||||
> Last release update: v0.2.56 — nostr_core_lib v0.6.10→v0.6.15: replace nostr_index with role_path, add signer_crypto tool, nostr_post PoW, nostr_signer_last_error surfacing. Wizard: toggle-based relay menu, admin keypair generation, reordered steps, existing-agent download/review, default role nostr_range, API key visible input
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
+55
-5
@@ -69,7 +69,8 @@ or the `signer.service_name` config field.
|
||||
"signer": {
|
||||
"mode": "nsigner_unix",
|
||||
"socket_name": "", // nsigner_unix: "" = auto-discover
|
||||
"role": "main",
|
||||
"role": "main", // REQUIRED for all nsigner_* modes
|
||||
"role_path": "m/44'/1237'/0'/0/0", // REQUIRED for all nsigner_* modes: full BIP-44 path
|
||||
"timeout_ms": 15000,
|
||||
"auth_privkey_hex": "", // nsigner_tcp only
|
||||
"tcp_host": "127.0.0.1", // nsigner_tcp
|
||||
@@ -78,16 +79,26 @@ or the `signer.service_name` config field.
|
||||
"target_qube": "nostr_signer", // nsigner_qrexec: target Qubes qube
|
||||
"service_name": "qubes.NsignerRpc" // nsigner_qrexec: qrexec service (default qubes.NsignerRpc)
|
||||
// nsigner_fds: fds_read_fd/fds_write_fd are CLI-only, NOT persisted here
|
||||
// derive_index: CLI-only (--signer-derive-index), NOT persisted here
|
||||
}
|
||||
```
|
||||
|
||||
> **Breaking change (n_signer v0.1.18+):** the `nostr_index` selector is
|
||||
> **removed**. n_signer now rejects it with `2006 nostr_index_deprecated`. The
|
||||
> only accepted selector for `nostr_*` verbs is `{"role":"<name>",
|
||||
> "role_path":"<full-path>"}` sent **together**. Role-only is rejected with
|
||||
> `2009 path_required`; path-only with `2008 role_required`. Update any
|
||||
> existing genesis configs that use `nostr_index` to use `role_path` instead.
|
||||
|
||||
### CLI flags
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|---------|
|
||||
| `--signer <mode>` | Select signer mode (overrides config/env). |
|
||||
| `--signer-socket <name>` | Abstract socket name for `nsigner_unix`. |
|
||||
| `--signer-role <role>` | `n_signer` role selector (default `main`). |
|
||||
| `--signer-role <role>` | `n_signer` role selector (default `main`). **Required** for all `nsigner_*` modes. |
|
||||
| `--signer-path <bip44-path>` | Full BIP-44 derivation path (e.g. `m/44'/1237'/0'/0/0`). **Required** for all `nsigner_*` modes — n_signer rejects role-only with `2009 path_required`. |
|
||||
| `--signer-derive-index <n>` | Algorithm index for the `derive` (HMAC) verb. Runtime-only; not persisted to genesis. |
|
||||
| `--signer-timeout <ms>` | Per-call timeout (default `15000`). |
|
||||
| `--signer-tcp <host:port>` | Shorthand for `--signer nsigner_tcp` + host/port. |
|
||||
| `--signer-serial <device>` | Shorthand for `--signer nsigner_serial` + device. |
|
||||
@@ -99,6 +110,7 @@ Precedence: CLI flag > `DIDACTYL_SIGNER` env var > genesis `signer.mode` >
|
||||
default (`local`).
|
||||
|
||||
In any `nsigner_*` mode, `--nsec` / `DIDACTYL_NSEC` are not required.
|
||||
`--signer-role` and `--signer-path` are both required.
|
||||
|
||||
## Interactive setup wizard
|
||||
|
||||
@@ -111,9 +123,13 @@ The wizard's **New agent → sign with a running n_signer** path and the
|
||||
- `f` d pair (prompts for `read_fd:write_fd` — advanced)
|
||||
- `e` xec qrexec (prompts for target qube + service name — Qubes cross-qube)
|
||||
|
||||
After a transport is chosen, the wizard runs a connectivity check
|
||||
(`nostr_signer_get_public_key`) and populates the agent pubkey before
|
||||
continuing.
|
||||
After a transport is chosen, the wizard prompts for a **role** (default
|
||||
`main`) and a **role_path** (BIP-44 derivation path, with presets for
|
||||
NIP-06 standard `m/44'/1237'/0'/0/0`, all-hardened
|
||||
`m/44'/1237'/0'/0'/0'`, or custom). Both are required — n_signer rejects
|
||||
role-only with `2009 path_required`. Then the wizard runs a connectivity
|
||||
check (`nostr_signer_get_public_key`) and populates the agent pubkey
|
||||
before continuing. On failure, the n_signer error string is displayed.
|
||||
|
||||
The **Existing agent** flow additionally lets you recover an agent **without
|
||||
entering an nsec** — kind-30078 config recall/publish is routed through the
|
||||
@@ -129,6 +145,40 @@ the nsec never enters the agent process during recovery.
|
||||
be embedded in `ExecStart`); choose **boot now** or wire fd-passing yourself
|
||||
via a wrapper unit.
|
||||
|
||||
## Agent tools (remote signer only)
|
||||
|
||||
Two agent tools leverage the remote signer for advanced operations:
|
||||
|
||||
### `nostr_post` — PoW mining (NIP-13)
|
||||
|
||||
The `nostr_post` tool accepts optional `difficulty`, `threads`, and
|
||||
`timeout_sec` arguments. When `difficulty > 0` and a remote signer is
|
||||
available, the event is mined through `nostr_signer_mine_event` before
|
||||
publishing. In local mode, PoW is not supported (the local backend does
|
||||
not implement `mine_event`); the tool returns a clear error.
|
||||
|
||||
### `signer_crypto` — algorithm-based n_signer verbs
|
||||
|
||||
A single unified tool exposes all algorithm-based n_signer verbs through
|
||||
an `operation` enum:
|
||||
|
||||
| Operation | Algorithms | Key args |
|
||||
|-----------|------------|----------|
|
||||
| `get_info` | n/a | — |
|
||||
| `get_public_key` | all | `algorithm`, `index` |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `algorithm`, `index`, `scheme`, `message_hex` |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `algorithm`, `index`, `scheme`, `message_hex`, `signature_hex` |
|
||||
| `encapsulate` | ml-kem-768 | `peer_pubkey_hex` |
|
||||
| `decapsulate` | ml-kem-768 | `index`, `ciphertext_hex` |
|
||||
| `derive_shared_secret` | x25519 | `index`, `peer_pubkey_hex` |
|
||||
| `derive_hmac` | secp256k1 | `data` (uses `--signer-derive-index`) |
|
||||
| `otp_encrypt` | otp | `plaintext_b64`, `encoding` |
|
||||
| `otp_decrypt` | otp | `ciphertext`, `encoding` |
|
||||
|
||||
All operations require a remote n_signer; local mode returns
|
||||
`NOSTR_ERROR_NOT_SUPPORTED`. See the n_signer README §4 for the full
|
||||
algorithm reference.
|
||||
|
||||
## Security tradeoff
|
||||
|
||||
`local` keeps the nsec in the agent process. If the agent is compromised, the
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# Setup Wizard Menu Reference
|
||||
|
||||
Proposed wizard flow. Each menu the interactive setup wizard presents, in the
|
||||
order an operator encounters them. Use this as a design reference.
|
||||
|
||||
---
|
||||
|
||||
## Main Menu
|
||||
|
||||
```
|
||||
╔═ Main Menu ═══════════════════════════════════════════════════╗
|
||||
║ Choose Setup Mode ║
|
||||
╠═══════════════════════════════════════════════════════════════╣
|
||||
║ n ew agent -- create a fresh identity ║
|
||||
║ e xisting -- start an already set up agent ║
|
||||
║ l oad -- boot from genesis.jsonc ║
|
||||
║ q uit ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Agent Flow (8 steps)
|
||||
|
||||
### Step 1 of 8 — Agent Profile
|
||||
|
||||
```
|
||||
Agent name [Didactyl]: _
|
||||
```
|
||||
Single prompt. Default `Didactyl`.
|
||||
|
||||
### Step 2 of 8 — Relay Configuration
|
||||
|
||||
```
|
||||
Current relays:
|
||||
1. ws://localhost:7777
|
||||
2. wss://relay.laantungir.net
|
||||
3. wss://relay.primal.net
|
||||
|
||||
a dd a relay
|
||||
r emove a relay (by number)
|
||||
d one -- use this list
|
||||
b ack
|
||||
q uit
|
||||
```
|
||||
|
||||
- **a** — prompt: `Relay URL (wss:// or ws://):`
|
||||
- **r** — prompt: `Remove which number?` (cannot remove last relay)
|
||||
- **d** — accept and continue
|
||||
- Starts from the default relay list; operator adds/removes before identity
|
||||
validation queries the network
|
||||
|
||||
### Step 3 of 8 — Identity
|
||||
|
||||
```
|
||||
g enerate a new Nostr keypair
|
||||
p rovide an existing nsec
|
||||
s ign with a running n_signer
|
||||
b ack
|
||||
q uit
|
||||
```
|
||||
|
||||
- **g** — generates a fresh keypair, shows npub+nsec, asks to confirm saved
|
||||
- **p** — prompts for nsec (echo-suppressed), derives pubkey
|
||||
- **s** — enters the **n_signer Transport** sub-menu (below)
|
||||
|
||||
### Step 4 of 8 — Identity Validation & Existing Agent Detection
|
||||
|
||||
After the identity is established (new keypair, imported nsec, or n_signer
|
||||
pubkey), the wizard queries the configured relays for a kind 10002 relay list
|
||||
and kind 30078 encrypted config for this pubkey.
|
||||
|
||||
**If NO existing agent data is found:**
|
||||
|
||||
```
|
||||
Agent kind 10002: NOT FOUND (new identity available)
|
||||
Admin profile: N/A
|
||||
|
||||
c ontinue to administrator setup
|
||||
q uit
|
||||
```
|
||||
|
||||
**If existing agent data IS found:**
|
||||
|
||||
```
|
||||
Agent kind 10002: FOUND
|
||||
Admin profile: <admin name or NOT FOUND>
|
||||
|
||||
This identity already has an agent configured on the network.
|
||||
|
||||
d ownload existing config and review it
|
||||
r estart with a fresh identity
|
||||
c ontinue anyway (overwrite existing state)
|
||||
q uit
|
||||
```
|
||||
|
||||
- **d** — downloads all relevant events (kind 10002 relay list, kind 30078
|
||||
encrypted config, kind 0 profile, kind 3 contacts) from the relays, decrypts
|
||||
the config, and presents everything on a single review page (see Step 4a
|
||||
below). This merges the "existing agent" recovery path into the new agent
|
||||
flow so the operator does not need to restart the wizard.
|
||||
- **r** — returns to Step 3 (Identity) to pick a different identity
|
||||
- **c** — continues to Step 5 (Administrator) as a fresh setup that will
|
||||
overwrite the existing state
|
||||
|
||||
#### Step 4a — Downloaded Config Review (only if `d` chosen)
|
||||
|
||||
```
|
||||
Downloaded configuration for this identity:
|
||||
|
||||
Name: <agent_name>
|
||||
Identity: <pubkey>...
|
||||
Admin: <admin pubkey or (not set)>
|
||||
LLM Provider: <provider or (not set)>
|
||||
LLM Model: <model or (not set)>
|
||||
LLM Base URL: <base_url or (not set)>
|
||||
LLM API Key: <masked or (not set)>
|
||||
Relays: <N> configured
|
||||
DM Protocol: <nip04/nip17/both>
|
||||
|
||||
e dit these settings
|
||||
b oot with this config now
|
||||
i nstall dedicated-user systemd service and boot
|
||||
q uit
|
||||
```
|
||||
|
||||
- **e** — enters the review/edit loop (admin, LLM, relays — same as the
|
||||
Existing Agent review loop), then returns here
|
||||
- **b** — boots immediately with the downloaded config
|
||||
- **i** — installs systemd service and boots
|
||||
|
||||
If the operator chooses **b** or **i**, the wizard skips Steps 5–8 and goes
|
||||
straight to launch — the downloaded config is already complete.
|
||||
|
||||
### Step 5 of 8 — Administrator
|
||||
|
||||
```
|
||||
Admin pubkey (npub1... or 64-char hex): _
|
||||
```
|
||||
Single prompt. Accepts npub or hex.
|
||||
|
||||
### Step 6 of 8 — LLM Provider
|
||||
|
||||
```
|
||||
LLM provider [openai]: _
|
||||
API key: _
|
||||
Model [gpt-4o-mini]: _
|
||||
Base URL [https://api.openai.org/v1]: _
|
||||
Max tokens [512]: _
|
||||
Temperature [0.7]: _
|
||||
```
|
||||
|
||||
### Step 7 of 8 — Default Skills
|
||||
|
||||
(silent — prepares default startup skill events)
|
||||
|
||||
### Step 8 of 8 — Review
|
||||
|
||||
```
|
||||
Name: <agent_name>
|
||||
Identity: <pubkey>...
|
||||
Admin: <admin>...
|
||||
LLM: <model> @ <base_url>
|
||||
Relays: <N> configured
|
||||
DM Protocol: nip04
|
||||
|
||||
b oot the agent now
|
||||
i nstall dedicated-user systemd service and boot
|
||||
s tart over
|
||||
q uit
|
||||
```
|
||||
|
||||
- **b** — publishes kind-30078 config, boots immediately
|
||||
- **i** — publishes config, installs systemd unit, starts service
|
||||
- **s** — resets config and returns to Main Menu
|
||||
|
||||
---
|
||||
|
||||
## n_signer Transport Sub-menu
|
||||
|
||||
Reached from Step 3 (`s`) or Existing Agent (`s`).
|
||||
|
||||
```
|
||||
u nix abstract 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)
|
||||
e xec -- Qubes cross-qube qrexec (target qube + service)
|
||||
b ack
|
||||
q uit
|
||||
```
|
||||
|
||||
### Per-transport prompts
|
||||
|
||||
**u (unix)**
|
||||
```
|
||||
Discovered n_signer abstract sockets:
|
||||
1. nsigner_hairy_dog
|
||||
Pick a number, type a custom name, or press Enter for auto-discovery.
|
||||
Socket name [1]: _
|
||||
```
|
||||
|
||||
**t (tcp)**
|
||||
```
|
||||
host:port [127.0.0.1:7777]: _
|
||||
Auth privkey hex (64 chars, optional, echo-suppressed): _
|
||||
```
|
||||
|
||||
**s (serial)**
|
||||
```
|
||||
Discovered serial devices:
|
||||
1. /dev/ttyACM0
|
||||
Pick a number or type a custom path.
|
||||
Device [1]: _
|
||||
```
|
||||
|
||||
**f (fds)**
|
||||
```
|
||||
read_fd:write_fd: _
|
||||
```
|
||||
|
||||
**e (qrexec)**
|
||||
```
|
||||
Target qube [nostr_signer]: _
|
||||
qrexec service [qubes.NsignerRpc]: _
|
||||
```
|
||||
|
||||
### Common role + path prompts (after transport chosen)
|
||||
|
||||
```
|
||||
Role [nostr_range]: _
|
||||
|
||||
Derivation path presets:
|
||||
1) m/44'/1237'/0'/0/0 (NIP-06 standard)
|
||||
2) m/44'/1237'/0'/0'/0' (all-hardened)
|
||||
3) custom
|
||||
Select preset [1]: _
|
||||
role_path [m/44'/1237'/0'/0/0]: _
|
||||
|
||||
Timeout ms [15000]: _
|
||||
```
|
||||
|
||||
Then a connectivity check runs:
|
||||
```
|
||||
Checking connectivity to n_signer (mode=nsigner_unix, ..., role=nostr_range, path=m/44'/1237'/0'/0/0)...
|
||||
Connectivity check: OK. Pubkey: <hex>
|
||||
```
|
||||
On failure, the n_signer error string is shown and the operator can retry.
|
||||
|
||||
---
|
||||
|
||||
## Existing Agent Flow
|
||||
|
||||
Kept as a separate entry point from the Main Menu for operators who know they
|
||||
want to recover an existing agent and do not want to go through the new-agent
|
||||
steps.
|
||||
|
||||
### Identity
|
||||
|
||||
```
|
||||
n sec -- enter the agent private key
|
||||
s ign with a running n_signer (no nsec required)
|
||||
q uit
|
||||
```
|
||||
|
||||
- **n** — prompt: `Enter your agent nsec (nsec1... or 64-char hex):` (echo-suppressed)
|
||||
- **s** — enters the **n_signer Transport** sub-menu (same as above)
|
||||
|
||||
### Config Recovery
|
||||
|
||||
If no kind 10002 relay list is found:
|
||||
```
|
||||
Relay list (kind 10002): NOT FOUND
|
||||
|
||||
n ew agent setup with this identity
|
||||
q uit to main menu
|
||||
```
|
||||
|
||||
### Recovered Configuration (review loop)
|
||||
|
||||
```
|
||||
Your agent was found. Change any of the following:
|
||||
|
||||
Name: <name>
|
||||
Identity: <pubkey>...
|
||||
Admin: <admin>
|
||||
LLM Provider: <provider>
|
||||
LLM Model: <model>
|
||||
LLM Base URL: <base_url>
|
||||
LLM API Key: <masked>
|
||||
Relays: <N> configured
|
||||
|
||||
a dmin pubkey
|
||||
l lm config
|
||||
r elay configuration
|
||||
c ontinue to launch options
|
||||
q uit
|
||||
```
|
||||
|
||||
- **a** — re-prompt admin pubkey
|
||||
- **l** — re-prompt LLM config
|
||||
- **r** — relay add/remove/done sub-menu (same as Step 2)
|
||||
- **c** — proceed to launch
|
||||
|
||||
### Launch Options
|
||||
|
||||
```
|
||||
Name: <name>
|
||||
Identity: <pubkey>...
|
||||
Admin: <admin>...
|
||||
LLM: <model> @ <base_url>
|
||||
Relays: <N> configured
|
||||
|
||||
b oot the agent now
|
||||
i nstall dedicated-user systemd service and boot
|
||||
q uit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Load Genesis Flow
|
||||
|
||||
```
|
||||
Path to genesis.jsonc: _
|
||||
```
|
||||
Single prompt. Loads and boots.
|
||||
|
||||
---
|
||||
|
||||
## Default Relays
|
||||
|
||||
```
|
||||
1. ws://localhost:7777
|
||||
2. wss://relay.laantungir.net
|
||||
3. wss://relay.primal.net
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of changes from previous design
|
||||
|
||||
1. **Default relays changed** to `ws://localhost:7777`,
|
||||
`wss://relay.laantungir.net`, `wss://relay.primal.net`
|
||||
2. **Step order changed**: Agent (1) → Relays (2) → Identity (3) → Validation
|
||||
(4) → Admin (5) → LLM (6) → Skills (7) → Review (8)
|
||||
3. **Step 4 redesigned**: if an existing agent is detected, the operator is
|
||||
notified and offered three choices: download existing config and review it
|
||||
(merges the recovery path inline), restart with a fresh identity, or
|
||||
continue anyway (overwrite). The download path shows all recovered settings
|
||||
on a single page and lets the operator edit or boot directly — skipping
|
||||
Steps 5–8 since the config is already complete.
|
||||
@@ -22,7 +22,8 @@
|
||||
// "signer": {
|
||||
// "mode": "nsigner_unix", // local | nsigner_unix | nsigner_tcp | nsigner_serial | nsigner_fds | nsigner_qrexec
|
||||
// "socket_name": "", // nsigner_unix: abstract socket name (without @); "" = auto-discover
|
||||
// "role": "main", // n_signer role selector
|
||||
// "role": "main", // n_signer role selector (required for all nsigner_* modes)
|
||||
// "role_path": "m/44'/1237'/0'/0/0", // full BIP-44 derivation path (required for all nsigner_* modes)
|
||||
// "timeout_ms": 15000, // per-call timeout
|
||||
// "auth_privkey_hex": "", // nsigner_tcp only: kind-27235 auth envelope privkey
|
||||
// "tcp_host": "127.0.0.1", // nsigner_tcp host
|
||||
@@ -31,6 +32,7 @@
|
||||
// "target_qube": "nostr_signer", // nsigner_qrexec: target Qubes qube name
|
||||
// "service_name": "qubes.NsignerRpc" // nsigner_qrexec: qrexec service name (default qubes.NsignerRpc)
|
||||
// // nsigner_fds: fds_read_fd/fds_write_fd are CLI-only (--signer-fds), NOT persisted here
|
||||
// // derive_index: CLI-only (--signer-derive-index), NOT persisted here
|
||||
// },
|
||||
|
||||
// ─── Encrypted Startup Config Events ───────────────────────────────
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# Update nostr_core_lib + n_signer Integration
|
||||
|
||||
## Context
|
||||
|
||||
`n_signer` completed a breaking wire-protocol migration. Our vendored
|
||||
`nostr_core_lib` (v0.6.10) is incompatible with current `n_signer`. The upstream
|
||||
lib is now v0.6.15. This plan updates the vendored copy and adapts didactyl to
|
||||
the new API, plus exposes the new additive features.
|
||||
|
||||
## Breaking changes (mandatory)
|
||||
|
||||
1. **Verb renames** — legacy names removed: `sign_event`→`nostr_sign_event`,
|
||||
`nip04_encrypt`→`nostr_nip44_encrypt`, etc. (handled inside the lib update)
|
||||
2. **Selector rewrite** — `nostr_index` is **removed** (rejected with error
|
||||
`2006 nostr_index_deprecated`). The only accepted selector for `nostr_*`
|
||||
verbs is now `{"role":"<name>","role_path":"<full-bip44-path>"}` sent
|
||||
**together**. Role-only → `2009 path_required`.
|
||||
|
||||
## Our exposure
|
||||
|
||||
- `nostr_signer_nsigner_set_nostr_index` in `src/main.c:515`,
|
||||
`src/setup_wizard.c:2513`
|
||||
- `nostr_index` config field in `src/config.h:152`
|
||||
- `--signer-index` CLI flag in `src/main.c`
|
||||
- `nostr_index` persisted to genesis
|
||||
|
||||
All remote-signer operations fail against current n_signer.
|
||||
|
||||
## New APIs (additive, full scope)
|
||||
|
||||
- `nostr_signer_last_error()` — human-readable error from last failed call
|
||||
- `nostr_signer_nsigner_set_role_path()` / `set_derive_index()` — replace
|
||||
set_nostr_index
|
||||
- `nostr_signer_nsigner_from_transport()` / `_from_client()` — flexible
|
||||
constructors
|
||||
- `nostr_signer_mine_event()` — NIP-13 PoW through the signer
|
||||
- Algorithm verbs: `sign`/`verify`/`encapsulate`/`decapsulate`/
|
||||
`derive_shared_secret`/`derive`/`encrypt`/`decrypt` (secp256k1, ed25519,
|
||||
x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp)
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Genesis/CLI] --> B[signer_config_t: role + role_path + derive_index]
|
||||
B --> C[construct_signer in main.c]
|
||||
C --> D[nostr_signer_nsigner_* factory]
|
||||
D --> E[nostr_signer_nsigner_set_role_path]
|
||||
E --> F[nostr_signer_get_public_key connectivity check]
|
||||
F --> G{success?}
|
||||
G -->|no| H[print nostr_signer_last_error + signer_health_record_failure]
|
||||
G -->|yes| I[Process-lifetime signer handle]
|
||||
I --> J[nostr_handler / tools_context / cashu_wallet / block_list]
|
||||
I --> K[signer_crypto tool + nostr_post PoW]
|
||||
K --> L{local mode?}
|
||||
L -->|yes| M[return NOT_SUPPORTED error]
|
||||
L -->|no| N[route through remote nsigner]
|
||||
```
|
||||
|
||||
## Design decisions
|
||||
|
||||
- **Phase 6:** Extend `nostr_post` with optional PoW args (no new tool). PoW is
|
||||
a posting variant; keeps agent tool count unchanged.
|
||||
- **Phase 7:** Single unified `signer_crypto` tool with `operation` enum
|
||||
covering all 10 algorithm verbs. Net +1 tool instead of +10.
|
||||
- **`role_path` required** for all `nsigner_*` modes (matches n_signer's hard
|
||||
requirement).
|
||||
- **`derive_index` is CLI-only** (like `fds`) — not persisted to genesis.
|
||||
- **Algorithm tools require remote signer** — local backend returns
|
||||
`NOSTR_ERROR_NOT_SUPPORTED`.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1 — Update vendored nostr_core_lib v0.6.10 → v0.6.15
|
||||
|
||||
- Copy upstream `nostr_core_lib/` over `didactyl/nostr_core_lib/`
|
||||
- Verify VERSION=0.6.15
|
||||
- Rebuild: `cd nostr_core_lib && ./build.sh --nips=001,004,005,006,011,013,017,019,021,042,044,046,059,060,061`
|
||||
- Confirm new headers present; `nostr_signer_nsigner_set_nostr_index` gone
|
||||
|
||||
### Phase 2 — Replace nostr_index with role_path in config layer
|
||||
|
||||
- `src/config.h`: remove `nostr_index`; add `role_path[OW_MAX_URL_LEN]`,
|
||||
`derive_index` (-1 = unset)
|
||||
- `src/config.c`: remove nostr_index parsing; add role_path parsing;
|
||||
derive_index is CLI-only
|
||||
- Update error messages
|
||||
|
||||
### Phase 3 — Update main.c signer construction + CLI flags
|
||||
|
||||
- Remove `--signer-index`; add `--signer-path <bip44-path>`,
|
||||
`--signer-derive-index <n>`
|
||||
- `apply_signer_overrides()`: require both role + role_path for nsigner_* modes
|
||||
- `construct_signer()`: `set_role_path` + `set_derive_index` instead of
|
||||
`set_nostr_index`
|
||||
- Surface `nostr_signer_last_error()` in failure paths
|
||||
- Update `print_usage()`
|
||||
|
||||
### Phase 4 — Update setup_wizard.c
|
||||
|
||||
- `prompt_signer_transport()`: remove nostr_index prompt; add role_path prompt
|
||||
with presets (NIP-06 default, hardened, custom)
|
||||
- `wizard_construct_ephemeral_signer()`: set_role_path instead of set_nostr_index
|
||||
- `install_system_service_with_dedicated_user()`: `--signer-path` in key_args;
|
||||
shell-escape the path (contains `'`)
|
||||
- `persist_runtime_config_to_nostr_wizard()`: role_path JSON field instead of
|
||||
nostr_index
|
||||
|
||||
### Phase 5 — Surface nostr_signer_last_error() everywhere
|
||||
|
||||
- `src/main.c`, `src/nostr_handler.c`, `src/nostr_block_list.c`,
|
||||
`src/tools/tool_nostr_dm.c`, `tool_memory.c`, `tool_config.c`,
|
||||
`tool_task.c`, `cashu_wallet.c`, `src/signer_health.c`
|
||||
|
||||
### Phase 6 — Extend nostr_post with optional PoW args
|
||||
|
||||
- Add `difficulty` (default 0), `threads` (default 1), `timeout_sec` (default
|
||||
600) to nostr_post schema
|
||||
- difficulty > 0 + remote signer → `nostr_signer_mine_event`
|
||||
- difficulty > 0 + local mode → clear error (PoW requires remote n_signer)
|
||||
- difficulty == 0 → unchanged
|
||||
|
||||
### Phase 7 — Single unified signer_crypto tool
|
||||
|
||||
- `src/tools/tool_signer_crypto.c`: one tool `signer_crypto` with `operation`
|
||||
enum: get_info | get_public_key | sign | verify | encapsulate | decapsulate |
|
||||
derive_shared_secret | derive_hmac | otp_encrypt | otp_decrypt
|
||||
- Operation-specific optional args: algorithm, index, scheme, message_hex,
|
||||
signature_hex, peer_pubkey_hex, ciphertext_hex, plaintext_b64, encoding, data
|
||||
- Requires ctx->signer != NULL; clear error otherwise
|
||||
- Add to tools_internal.h, tools_dispatch.c, tools_schema.c, Makefile SRCS
|
||||
|
||||
### Phase 8 — Docs + schema
|
||||
|
||||
- `genesis.jsonc.example`, `schemas.json`: role_path instead of nostr_index
|
||||
- `docs/SIGNER.md`: role+role_path, --signer-path, signer_crypto tool, PoW,
|
||||
nostr_index removed (breaking)
|
||||
- `plans/wizard_full_nsigner_transports.md`: mark superseded
|
||||
|
||||
### Phase 9 — Build, test, verify
|
||||
|
||||
- `make clean && make deps && make`
|
||||
- Smoke test with n_signer: `--signer nsigner_unix --signer-socket <name>
|
||||
--signer-role main --signer-path "m/44'/1237'/0'/0/0"`
|
||||
- Test signer_crypto (get_info, sign+verify roundtrip)
|
||||
- Test nostr_post with difficulty=4
|
||||
- Run `tests/run_tests.py`
|
||||
+13
-9
@@ -280,16 +280,19 @@ static int parse_signer_config(cJSON* root, didactyl_config_t* config) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional nostr_index: numeric key index selector. When set (>= 0) it
|
||||
* overrides role on the n_signer side. -1 (default) means use role. */
|
||||
cJSON* nostr_index = cJSON_GetObjectItemCaseSensitive(signer, "nostr_index");
|
||||
if (nostr_index && cJSON_IsNumber(nostr_index)) {
|
||||
int idx = (int)nostr_index->valuedouble;
|
||||
if (idx >= 0) {
|
||||
config->signer.nostr_index = idx;
|
||||
}
|
||||
/* role_path: full BIP-44 derivation path (e.g. "m/44'/1237'/0'/0/0").
|
||||
* Required for all nsigner_* modes — n_signer now rejects role-only
|
||||
* selectors with 2009 path_required and bare nostr_index with 2006
|
||||
* nostr_index_deprecated. The role+role_path pair is sent together. */
|
||||
if (copy_json_string(signer, "role_path", config->signer.role_path,
|
||||
sizeof(config->signer.role_path), 0) != 0) {
|
||||
config_set_error("signer.role_path must be a string when provided");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* derive_index is intentionally NOT parsed from JSON — it is CLI/runtime-
|
||||
* only (like fds_read_fd/fds_write_fd) and cannot be persisted to genesis. */
|
||||
|
||||
if (copy_json_string(signer, "auth_privkey_hex", config->signer.auth_privkey_hex,
|
||||
sizeof(config->signer.auth_privkey_hex), 0) != 0) {
|
||||
config_set_error("signer.auth_privkey_hex must be a string when provided");
|
||||
@@ -1625,7 +1628,8 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
snprintf(config->signer.mode, sizeof(config->signer.mode), "%s", "local");
|
||||
config->signer.socket_name[0] = '\0';
|
||||
snprintf(config->signer.role, sizeof(config->signer.role), "%s", "main");
|
||||
config->signer.nostr_index = -1; /* -1 = unset; use role selector */
|
||||
config->signer.role_path[0] = '\0'; /* required for nsigner_* modes; set via --signer-path or signer.role_path */
|
||||
config->signer.derive_index = -1; /* -1 = unset; CLI-only (--signer-derive-index) */
|
||||
config->signer.timeout_ms = 15000;
|
||||
config->signer.auth_privkey_hex[0] = '\0';
|
||||
config->signer.tcp_host[0] = '\0';
|
||||
|
||||
+6
-2
@@ -148,8 +148,12 @@ typedef struct {
|
||||
typedef struct {
|
||||
char mode[OW_MAX_SIGNER_MODE_LEN]; /* "local" | "nsigner_unix" | "nsigner_tcp" | "nsigner_serial" | "nsigner_fds" | "nsigner_qrexec" */
|
||||
char socket_name[OW_MAX_SIGNER_SOCKET_LEN]; /* nsigner_unix: abstract socket name (without @); "" = auto-discover */
|
||||
char role[OW_MAX_SIGNER_ROLE_LEN]; /* n_signer role selector (default "main") */
|
||||
int nostr_index; /* n_signer key index selector (-1 = unset; use role). When set, overrides role. */
|
||||
char role[OW_MAX_SIGNER_ROLE_LEN]; /* n_signer role selector (default "main"). Required for all nsigner_* modes. */
|
||||
char role_path[OW_MAX_URL_LEN]; /* full BIP-44 derivation path (e.g. "m/44'/1237'/0'/0/0"). Required for
|
||||
* all nsigner_* modes — n_signer rejects role-only with 2009 path_required
|
||||
* and bare nostr_index with 2006 nostr_index_deprecated. */
|
||||
int derive_index; /* algorithm index for the derive verb (HMAC). -1 = unset (default).
|
||||
* CLI/runtime-only — NOT persisted to genesis. Set via --signer-derive-index. */
|
||||
int timeout_ms; /* per-call timeout (default 15000) */
|
||||
char auth_privkey_hex[OW_MAX_SIGNER_AUTH_HEX_LEN]; /* optional, TCP auth envelope only */
|
||||
/* nsigner_tcp parsed host/port (populated from --signer-tcp or config) */
|
||||
|
||||
@@ -30,9 +30,11 @@ static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = {
|
||||
#define DIDACTYL_DEFAULT_KIND10050_CONTENT ""
|
||||
|
||||
static const char* DIDACTYL_DEFAULT_RELAYS[] = {
|
||||
"ws://localhost:7777",
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.primal.net"
|
||||
"wss://relay.primal.net",
|
||||
"wss://relay.zapstore.dev"
|
||||
};
|
||||
|
||||
#define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0])))
|
||||
|
||||
+79
-24
@@ -188,8 +188,11 @@ static void print_usage(const char* prog) {
|
||||
" qube name (service defaults to qubes.NsignerRpc).\n"
|
||||
" --signer-service <name>\n"
|
||||
" qrexec service name for nsigner_qrexec (default: qubes.NsignerRpc).\n"
|
||||
" --signer-index <n>\n"
|
||||
" n_signer key index selector (overrides --signer-role). -1 = use role.\n"
|
||||
" --signer-path <bip44-path>\n"
|
||||
" Full BIP-44 derivation path (e.g. m/44'/1237'/0'/0/0). REQUIRED for\n"
|
||||
" all nsigner_* modes — n_signer rejects role-only with 2009 path_required.\n"
|
||||
" --signer-derive-index <n>\n"
|
||||
" Algorithm index for the derive (HMAC) verb. Runtime-only; not persisted.\n"
|
||||
" --dump-schemas\n"
|
||||
" Print tool schemas JSON and exit.\n"
|
||||
" --test-tool <name> <args_json>\n"
|
||||
@@ -215,16 +218,20 @@ static void print_usage(const char* prog) {
|
||||
" %s --config genesis.jsonc --api-bind 127.0.0.1 --api-port 8484\n"
|
||||
"\n"
|
||||
" 5) Sign with a running n_signer (separate the nsec from the agent)\n"
|
||||
" %s --signer nsigner_unix --signer-socket nsigner_hairy_dog --admin npub1...\n"
|
||||
" %s --signer nsigner_unix --signer-socket nsigner_hairy_dog \\\n"
|
||||
" --signer-role main --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
|
||||
"\n"
|
||||
" 6) n_signer via TCP\n"
|
||||
" %s --signer-tcp 127.0.0.1:7777 --admin npub1...\n"
|
||||
" %s --signer-tcp 127.0.0.1:7777 --signer-role main \\\n"
|
||||
" --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
|
||||
"\n"
|
||||
" 7) n_signer via USB serial\n"
|
||||
" %s --signer-serial /dev/ttyACM0 --admin npub1...\n"
|
||||
" %s --signer-serial /dev/ttyACM0 --signer-role main \\\n"
|
||||
" --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
|
||||
"\n"
|
||||
" 8) n_signer in another Qubes qube (qrexec, no network)\n"
|
||||
" %s --signer-qrexec nostr_signer --admin npub1...\n"
|
||||
" %s --signer-qrexec nostr_signer --signer-role main \\\n"
|
||||
" --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
|
||||
"\n"
|
||||
" 9) Dump tool schemas\n"
|
||||
" %s --dump-schemas\n"
|
||||
@@ -311,7 +318,8 @@ static int apply_signer_overrides(didactyl_config_t* cfg,
|
||||
const char* cli_fds,
|
||||
const char* cli_qrexec,
|
||||
const char* cli_service,
|
||||
int cli_index) {
|
||||
const char* cli_path,
|
||||
int cli_derive_index) {
|
||||
if (!cfg) {
|
||||
return -1;
|
||||
}
|
||||
@@ -400,10 +408,36 @@ static int apply_signer_overrides(didactyl_config_t* cfg,
|
||||
if (cli_timeout > 0) {
|
||||
cfg->signer.timeout_ms = cli_timeout;
|
||||
}
|
||||
/* --signer-index overrides the genesis nostr_index. -1 means unset (use role).
|
||||
* Any non-negative value selects a specific key index on the n_signer side. */
|
||||
if (cli_index >= 0) {
|
||||
cfg->signer.nostr_index = cli_index;
|
||||
/* --signer-path overrides the genesis role_path. Required for all nsigner_*
|
||||
* modes — n_signer now rejects role-only selectors with 2009 path_required
|
||||
* and bare nostr_index with 2006 nostr_index_deprecated. */
|
||||
if (cli_path && cli_path[0] != '\0') {
|
||||
snprintf(cfg->signer.role_path, sizeof(cfg->signer.role_path), "%s", cli_path);
|
||||
}
|
||||
/* --signer-derive-index sets the algorithm index for the derive (HMAC) verb.
|
||||
* Runtime-only; not persisted to genesis. -1 means unset. */
|
||||
if (cli_derive_index >= 0) {
|
||||
cfg->signer.derive_index = cli_derive_index;
|
||||
}
|
||||
|
||||
/* For all remote (nsigner_*) modes, require BOTH role and role_path.
|
||||
* n_signer rejects role-only with 2009 path_required and path-only with
|
||||
* 2008 role_required. This catches misconfiguration early with a clear
|
||||
* message instead of a cryptic RPC error at first sign attempt. */
|
||||
if (signer_mode_is_remote_local(mode)) {
|
||||
if (cfg->signer.role[0] == '\0') {
|
||||
fprintf(stderr, "%s mode requires --signer-role <role> or signer.role in "
|
||||
"config (n_signer rejects path-only with 2008 role_required)\n",
|
||||
mode);
|
||||
return -1;
|
||||
}
|
||||
if (cfg->signer.role_path[0] == '\0') {
|
||||
fprintf(stderr, "%s mode requires --signer-path <bip44-path> or signer.role_path "
|
||||
"in config (n_signer rejects role-only with 2009 path_required). "
|
||||
"Example: --signer-path \"m/44'/1237'/0'/0/0\"\n",
|
||||
mode);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -510,11 +544,20 @@ static nostr_signer_t* construct_signer(didactyl_config_t* cfg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Apply nostr_index selector when set (overrides role on the n_signer side). */
|
||||
if (signer && cfg->signer.nostr_index >= 0) {
|
||||
if (nostr_signer_nsigner_set_nostr_index(signer, cfg->signer.nostr_index) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Warning: failed to set nostr_index=%d on signer (mode=%s)\n",
|
||||
cfg->signer.nostr_index, mode);
|
||||
/* Apply role_path selector (required for all nsigner_* modes — n_signer
|
||||
* rejects role-only with 2009 path_required). The role was already passed
|
||||
* to the factory constructor; set_role_path adds the full BIP-44 path. */
|
||||
if (signer && cfg->signer.role_path[0] != '\0') {
|
||||
if (nostr_signer_nsigner_set_role_path(signer, cfg->signer.role_path) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Warning: failed to set role_path='%s' on signer (mode=%s)\n",
|
||||
cfg->signer.role_path, mode);
|
||||
}
|
||||
}
|
||||
/* Apply derive_index for the algorithm-based derive (HMAC) verb. -1 = unset. */
|
||||
if (signer && cfg->signer.derive_index >= 0) {
|
||||
if (nostr_signer_nsigner_set_derive_index(signer, cfg->signer.derive_index) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Warning: failed to set derive_index=%d on signer (mode=%s)\n",
|
||||
cfg->signer.derive_index, mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,9 +565,10 @@ static nostr_signer_t* construct_signer(didactyl_config_t* cfg) {
|
||||
if (!signer) {
|
||||
const char* sock = cfg->signer.socket_name[0] ? cfg->signer.socket_name : "<auto>";
|
||||
fprintf(stderr, "Failed to construct remote signer (mode=%s, socket=%s, role=%s, "
|
||||
"timeout_ms=%d): nostr_signer_nsigner_* returned NULL\n",
|
||||
"role_path=%s, timeout_ms=%d): nostr_signer_nsigner_* returned NULL\n",
|
||||
mode, strcmp(mode, "nsigner_unix") == 0 ? sock : "<other>",
|
||||
cfg->signer.role[0] ? cfg->signer.role : "main",
|
||||
cfg->signer.role_path[0] ? cfg->signer.role_path : "<unset>",
|
||||
cfg->signer.timeout_ms);
|
||||
signer_health_record_failure(NOSTR_ERROR_IO_FAILED, "signer construction returned NULL");
|
||||
return NULL;
|
||||
@@ -535,13 +579,20 @@ static nostr_signer_t* construct_signer(didactyl_config_t* cfg) {
|
||||
int gpk_rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (gpk_rc != NOSTR_SUCCESS) {
|
||||
const char* sock = cfg->signer.socket_name[0] ? cfg->signer.socket_name : "<auto>";
|
||||
const char* last_err = nostr_signer_last_error(signer);
|
||||
fprintf(stderr, "Signer connectivity check failed (mode=%s, socket=%s, role=%s, "
|
||||
"timeout_ms=%d, rc=%d): could not retrieve public key from n_signer. "
|
||||
"Is the n_signer process running and reachable?\n",
|
||||
"role_path=%s, timeout_ms=%d, rc=%d): could not retrieve public key "
|
||||
"from n_signer. Is the n_signer process running and reachable?\n",
|
||||
mode, strcmp(mode, "nsigner_unix") == 0 ? sock : "<other>",
|
||||
cfg->signer.role[0] ? cfg->signer.role : "main",
|
||||
cfg->signer.role_path[0] ? cfg->signer.role_path : "<unset>",
|
||||
cfg->signer.timeout_ms, gpk_rc);
|
||||
signer_health_record_failure(gpk_rc, "startup connectivity check (get_public_key)");
|
||||
if (last_err && last_err[0] != '\0') {
|
||||
fprintf(stderr, " n_signer error: %s\n", last_err);
|
||||
signer_health_record_failure(gpk_rc, last_err);
|
||||
} else {
|
||||
signer_health_record_failure(gpk_rc, "startup connectivity check (get_public_key)");
|
||||
}
|
||||
nostr_signer_free(signer);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1602,7 +1653,8 @@ int main(int argc, char** argv) {
|
||||
const char* cli_signer_fds = NULL; /* read_fd:write_fd shorthand */
|
||||
const char* cli_signer_qrexec = NULL; /* target qube shorthand */
|
||||
const char* cli_signer_service = NULL; /* qrexec service name */
|
||||
int cli_signer_index = -1; /* n_signer key index (-1 = unset) */
|
||||
const char* cli_signer_path = NULL; /* full BIP-44 derivation path */
|
||||
int cli_signer_derive_index = -1; /* derive verb algorithm index (-1 = unset) */
|
||||
didactyl_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
nostr_signer_t* g_signer = NULL;
|
||||
@@ -1685,8 +1737,10 @@ int main(int argc, char** argv) {
|
||||
cli_signer_qrexec = argv[++i];
|
||||
} else if (strcmp(argv[i], "--signer-service") == 0 && i + 1 < argc) {
|
||||
cli_signer_service = argv[++i];
|
||||
} else if (strcmp(argv[i], "--signer-index") == 0 && i + 1 < argc) {
|
||||
cli_signer_index = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--signer-path") == 0 && i + 1 < argc) {
|
||||
cli_signer_path = argv[++i];
|
||||
} else if (strcmp(argv[i], "--signer-derive-index") == 0 && i + 1 < argc) {
|
||||
cli_signer_derive_index = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--dump-schemas") == 0) {
|
||||
dump_schemas = 1;
|
||||
} else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) {
|
||||
@@ -1737,7 +1791,8 @@ int main(int argc, char** argv) {
|
||||
cli_signer_role, cli_signer_timeout,
|
||||
cli_signer_tcp, cli_signer_serial,
|
||||
cli_signer_fds, cli_signer_qrexec,
|
||||
cli_signer_service, cli_signer_index) != 0) {
|
||||
cli_signer_service, cli_signer_path,
|
||||
cli_signer_derive_index) != 0) {
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 2
|
||||
#define DIDACTYL_VERSION_PATCH 55
|
||||
#define DIDACTYL_VERSION "v0.2.55"
|
||||
#define DIDACTYL_VERSION_PATCH 56
|
||||
#define DIDACTYL_VERSION "v0.2.56"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -204,6 +204,9 @@ static int decrypt_private_tags_array(const char* encrypted_content, cJSON** out
|
||||
encrypted_content,
|
||||
&plaintext);
|
||||
if (rc != NOSTR_SUCCESS || !plaintext) {
|
||||
const char* le = nostr_signer_last_error(g_block_signer);
|
||||
DEBUG_ERROR("[block_list] nip44_decrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
plain_src = plaintext;
|
||||
@@ -254,6 +257,9 @@ static int encrypt_private_tags_array(cJSON* private_tags_array, char** out_cont
|
||||
&cipher);
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS || !cipher) {
|
||||
const char* le = nostr_signer_last_error(g_block_signer);
|
||||
DEBUG_ERROR("[block_list] nip44_encrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_content = cipher;
|
||||
|
||||
+70
-5
@@ -1063,7 +1063,8 @@ static int nip44_encrypt_self_local(const char* plaintext, char** out_ciphertext
|
||||
plain,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
signer_health_record_failure(rc, "nip44_encrypt(self)");
|
||||
const char* le = nostr_signer_last_error(g_auth_signer);
|
||||
signer_health_record_failure(rc, (le && le[0]) ? le : "nip44_encrypt(self)");
|
||||
return -1;
|
||||
}
|
||||
signer_health_record_ok();
|
||||
@@ -1105,7 +1106,8 @@ static int nip44_decrypt_self_local(const char* ciphertext, char** out_plaintext
|
||||
ciphertext,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
signer_health_record_failure(rc, "nip44_decrypt(self)");
|
||||
const char* le = nostr_signer_last_error(g_auth_signer);
|
||||
signer_health_record_failure(rc, (le && le[0]) ? le : "nip44_decrypt(self)");
|
||||
return -1;
|
||||
}
|
||||
signer_health_record_ok();
|
||||
@@ -1971,8 +1973,10 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
content->valuestring,
|
||||
&signer_out);
|
||||
if (dec_rc != NOSTR_SUCCESS || !signer_out) {
|
||||
signer_health_record_failure(dec_rc, "nip04_decrypt(incoming DM)");
|
||||
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", sender_pubkey_hex);
|
||||
const char* le = nostr_signer_last_error(g_auth_signer);
|
||||
signer_health_record_failure(dec_rc, (le && le[0]) ? le : "nip04_decrypt(incoming DM)");
|
||||
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s... (rc=%d, err=%s)\n",
|
||||
sender_pubkey_hex, dec_rc, (le && le[0]) ? le : "<no detail>");
|
||||
return;
|
||||
}
|
||||
signer_health_record_ok();
|
||||
@@ -2592,6 +2596,10 @@ void nostr_handler_set_signer(nostr_signer_t* signer) {
|
||||
g_auth_signer = signer;
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_handler_get_signer(void) {
|
||||
return g_auth_signer;
|
||||
}
|
||||
|
||||
int nostr_handler_init(didactyl_config_t* config) {
|
||||
if (!config) {
|
||||
return -1;
|
||||
@@ -3239,7 +3247,8 @@ int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex,
|
||||
message,
|
||||
&encrypted);
|
||||
if (enc_rc != NOSTR_SUCCESS || !encrypted) {
|
||||
signer_health_record_failure(enc_rc, "nip04_encrypt(outgoing DM)");
|
||||
const char* le = nostr_signer_last_error(g_auth_signer);
|
||||
signer_health_record_failure(enc_rc, (le && le[0]) ? le : "nip04_encrypt(outgoing DM)");
|
||||
return -1;
|
||||
}
|
||||
signer_health_record_ok();
|
||||
@@ -3513,6 +3522,62 @@ int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags,
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Publish a pre-signed event (e.g. from nostr_signer_mine_event) through the
|
||||
* relay pool. Returns 0 on success, -1 on failure. */
|
||||
int nostr_handler_publish_event(cJSON* signed_event, nostr_publish_result_t* out_result) {
|
||||
if (!g_cfg || !g_pool || !signed_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_publish_targets("publish pre-signed event");
|
||||
|
||||
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
|
||||
if (!connected_relays) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int connected_count = 0;
|
||||
for (int i = 0; i < g_cfg->relay_count; i++) {
|
||||
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
|
||||
connected_relays[connected_count++] = g_cfg->relays[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (connected_count <= 0) {
|
||||
DEBUG_WARN("[didactyl] pre-signed event not queued: no connected relays");
|
||||
free(connected_relays);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* event_copy = cJSON_Duplicate(signed_event, 1);
|
||||
if (!event_copy) {
|
||||
free(connected_relays);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sent = nostr_relay_pool_publish_async(
|
||||
g_pool,
|
||||
connected_relays,
|
||||
connected_count,
|
||||
event_copy,
|
||||
NULL,
|
||||
NULL);
|
||||
|
||||
cJSON* kind_json = cJSON_GetObjectItemCaseSensitive(signed_event, "kind");
|
||||
int kind = (kind_json && cJSON_IsNumber(kind_json)) ? (int)kind_json->valuedouble : 0;
|
||||
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(signed_event, "id");
|
||||
const char* event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring)
|
||||
? event_id->valuestring
|
||||
: "";
|
||||
|
||||
fill_publish_result(out_result, kind, NULL, event_id_hex,
|
||||
connected_relays, connected_count, sent);
|
||||
|
||||
cJSON_Delete(event_copy);
|
||||
free(connected_relays);
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
|
||||
if (!g_cfg || !g_pool || !filter) {
|
||||
return NULL;
|
||||
|
||||
@@ -48,6 +48,9 @@ int nostr_handler_init(didactyl_config_t* config);
|
||||
* nostr_handler_cleanup. If not called, the legacy raw-key AUTH path is used
|
||||
* (backward compatible). */
|
||||
void nostr_handler_set_signer(nostr_signer_t* signer);
|
||||
/* Returns the process-lifetime signer handle (may be NULL in local mode).
|
||||
* Safe to call at any point after nostr_handler_set_signer. */
|
||||
nostr_signer_t* nostr_handler_get_signer(void);
|
||||
|
||||
void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager);
|
||||
int nostr_handler_subscribe_admin_context(void);
|
||||
@@ -66,6 +69,10 @@ int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex,
|
||||
const char* message,
|
||||
didactyl_dm_history_role_t role);
|
||||
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result);
|
||||
/* Publish a pre-signed event (e.g. from nostr_signer_mine_event) through the
|
||||
* relay pool. The event must have id, pubkey, sig, and all required fields
|
||||
* already populated. Returns 0 on success, -1 on failure. */
|
||||
int nostr_handler_publish_event(cJSON* signed_event, nostr_publish_result_t* out_result);
|
||||
void nostr_handler_publish_result_free(nostr_publish_result_t* result);
|
||||
char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
|
||||
|
||||
+488
-175
@@ -331,7 +331,8 @@ static void config_set_defaults(didactyl_config_t* cfg) {
|
||||
* signer.mode after the wizard returns BOOTSTRAP for local paths. */
|
||||
snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", "local");
|
||||
snprintf(cfg->signer.role, sizeof(cfg->signer.role), "%s", "main");
|
||||
cfg->signer.nostr_index = -1; /* -1 = unset; use role selector */
|
||||
cfg->signer.role_path[0] = '\0'; /* required for nsigner_* modes; set via --signer-path or signer.role_path */
|
||||
cfg->signer.derive_index = -1; /* -1 = unset; CLI-only (--signer-derive-index) */
|
||||
cfg->signer.timeout_ms = 15000;
|
||||
}
|
||||
|
||||
@@ -1281,8 +1282,8 @@ static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg,
|
||||
cJSON_AddStringToObject(signer_obj, "mode", cfg->signer.mode);
|
||||
cJSON_AddStringToObject(signer_obj, "socket_name", cfg->signer.socket_name);
|
||||
cJSON_AddStringToObject(signer_obj, "role", cfg->signer.role);
|
||||
if (cfg->signer.nostr_index >= 0) {
|
||||
cJSON_AddNumberToObject(signer_obj, "nostr_index", cfg->signer.nostr_index);
|
||||
if (cfg->signer.role_path[0] != '\0') {
|
||||
cJSON_AddStringToObject(signer_obj, "role_path", cfg->signer.role_path);
|
||||
}
|
||||
cJSON_AddNumberToObject(signer_obj, "timeout_ms", cfg->signer.timeout_ms);
|
||||
cJSON_AddItemToObject(user_settings, "signer", signer_obj);
|
||||
@@ -1614,23 +1615,56 @@ static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg,
|
||||
char input[WIZARD_LINE_MAX];
|
||||
for (;;) {
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (line_is_quit(input)) return -1;
|
||||
fprintf(stderr, " Enter the admin's Nostr public key, or generate a fresh one.\n\n");
|
||||
print_option('e', "nter an existing npub1... or hex pubkey");
|
||||
print_option('g', "enerate a fresh admin keypair");
|
||||
print_option('q', "uit");
|
||||
wizard_option_t opts[] = {{'e', ""}, {'g', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 3);
|
||||
if (c == 'q') return -1;
|
||||
|
||||
char hex[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(input, hex) != 0) {
|
||||
fprintf(stderr, "%sInvalid admin pubkey. Use npub1... or 64-char hex.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
if (c == 'g') {
|
||||
unsigned char priv[32], pub[32];
|
||||
if (nostr_generate_keypair(priv, pub) != 0) {
|
||||
fprintf(stderr, "%sFailed to generate keypair.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
char nsec[OW_MAX_KEY_LEN] = {0};
|
||||
char npub[OW_MAX_KEY_LEN] = {0};
|
||||
if (nostr_key_to_bech32(priv, "nsec", nsec) != 0 || nostr_key_to_bech32(pub, "npub", npub) != 0) {
|
||||
fprintf(stderr, "%sFailed to encode generated keypair.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
nostr_bytes_to_hex(pub, 32, cfg->admin.pubkey);
|
||||
fprintf(stderr, "\n Generated admin identity:\n");
|
||||
fprintf(stderr, " npub: %s\n", npub);
|
||||
fprintf(stderr, "%s nsec: %s%s\n", ANSI_YELLOW, nsec, ANSI_RESET);
|
||||
fprintf(stderr, "%s Save this nsec now. It will not be persisted.%s\n", ANSI_YELLOW, ANSI_RESET);
|
||||
char ok[WIZARD_LINE_MAX] = {0};
|
||||
if (read_line_prompt(" Type 'ok' when saved (or q to quit): ", ok, sizeof(ok)) != 0) return -1;
|
||||
if (line_is_quit(ok)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (c == 'e') {
|
||||
if (read_line_prompt(" Admin pubkey (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (line_is_quit(input)) return -1;
|
||||
|
||||
char hex[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(input, hex) != 0) {
|
||||
fprintf(stderr, "%sInvalid admin pubkey. Use npub1... or 64-char hex.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", hex);
|
||||
return 0;
|
||||
}
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", hex);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator");
|
||||
return prompt_admin_pubkey_with_header(cfg, "Step 5 of 8", "New Agent Setup -- Administrator");
|
||||
}
|
||||
|
||||
static int prompt_llm_config_with_header(didactyl_config_t* cfg,
|
||||
@@ -1667,7 +1701,7 @@ static int prompt_llm_config_with_header(didactyl_config_t* cfg,
|
||||
}
|
||||
|
||||
char api_key[OW_MAX_KEY_LEN] = {0};
|
||||
if (read_secret_prompt(" API Key: ", api_key, sizeof(api_key)) != 0) return -1;
|
||||
if (read_line_prompt(" API Key: ", api_key, sizeof(api_key)) != 0) return -1;
|
||||
if (line_is_quit(api_key)) return -1;
|
||||
|
||||
llm_config_t probe = cfg->llm;
|
||||
@@ -1759,71 +1793,160 @@ static int prompt_llm_config_with_header(didactyl_config_t* cfg,
|
||||
}
|
||||
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
return prompt_llm_config_with_header(cfg, "Step 6 of 8", "New Agent Setup -- LLM Provider");
|
||||
}
|
||||
|
||||
#define RELAY_TOGGLE_MAX 64
|
||||
|
||||
static int prompt_relay_configuration_with_header(didactyl_config_t* cfg,
|
||||
const char* step_title,
|
||||
const char* section_title) {
|
||||
char input[WIZARD_LINE_MAX];
|
||||
/* Build a local list of all known relays with enabled/disabled state.
|
||||
* Start with the current cfg->relays (all enabled). */
|
||||
char* all_relays[RELAY_TOGGLE_MAX];
|
||||
int relay_enabled[RELAY_TOGGLE_MAX];
|
||||
int all_count = 0;
|
||||
|
||||
/* Seed from the current config relays (all enabled). */
|
||||
for (int i = 0; i < cfg->relay_count && all_count < RELAY_TOGGLE_MAX; i++) {
|
||||
all_relays[all_count] = strdup(cfg->relays[i] ? cfg->relays[i] : "");
|
||||
relay_enabled[all_count] = 1;
|
||||
all_count++;
|
||||
}
|
||||
|
||||
/* Add any default relays that aren't already in the list (disabled by default). */
|
||||
for (int i = 0; i < DIDACTYL_DEFAULT_RELAY_COUNT && all_count < RELAY_TOGGLE_MAX; i++) {
|
||||
int found = 0;
|
||||
for (int j = 0; j < all_count; j++) {
|
||||
if (all_relays[j] && strcmp(all_relays[j], DIDACTYL_DEFAULT_RELAYS[i]) == 0) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
all_relays[all_count] = strdup(DIDACTYL_DEFAULT_RELAYS[i]);
|
||||
relay_enabled[all_count] = 0;
|
||||
all_count++;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
fprintf(stderr, " Current relays:\n");
|
||||
for (int i = 0; i < cfg->relay_count; i++) {
|
||||
fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : "");
|
||||
fprintf(stderr, " Relays (enter a number to toggle, [X] = enabled):\n");
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
fprintf(stderr, " %d. [%c] %s\n", i + 1,
|
||||
relay_enabled[i] ? 'X' : ' ',
|
||||
all_relays[i] ? all_relays[i] : "");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
print_option('a', "dd a relay");
|
||||
print_option('r', "emove a relay (by number)");
|
||||
print_option('d', "one -- use this list");
|
||||
print_option('b', "ack");
|
||||
print_option('q', "uit");
|
||||
fprintf(stderr, " Commands: a=add, d=done, b=back, q=quit\n");
|
||||
if (read_line_prompt("> ", input, sizeof(input)) != 0) return -1;
|
||||
if (line_is_quit(input)) return -1;
|
||||
|
||||
wizard_option_t opts[] = {
|
||||
{'a', "dd"}, {'r', "emove"}, {'d', "one"}, {'b', "ack"}, {'q', "uit"}
|
||||
};
|
||||
char c = read_menu_choice(opts, 5);
|
||||
if (c == 'q') return -1;
|
||||
if (c == 'b') return 1;
|
||||
if (c == 'd') {
|
||||
if (cfg->relay_count <= 0) {
|
||||
fprintf(stderr, "%sAt least one relay is required.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
return 0;
|
||||
/* Trim whitespace. */
|
||||
char* trimmed = input;
|
||||
while (*trimmed == ' ') trimmed++;
|
||||
if (trimmed[0] == '\0') continue;
|
||||
|
||||
/* Check if it's a number (toggle a relay). */
|
||||
int is_number = 1;
|
||||
for (size_t i = 0; trimmed[i] != '\0'; i++) {
|
||||
if (!isdigit((unsigned char)trimmed[i])) { is_number = 0; break; }
|
||||
}
|
||||
if (c == 'a') {
|
||||
if (read_line_prompt("Relay URL (wss:// or ws://): ", input, sizeof(input)) != 0) return -1;
|
||||
if (line_is_quit(input)) return -1;
|
||||
if (!(strncmp(input, "wss://", 6) == 0 || strncmp(input, "ws://", 5) == 0)) {
|
||||
fprintf(stderr, "%sRelay must start with wss:// or ws://%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
if (relay_add(cfg, input) != 0) {
|
||||
fprintf(stderr, "%sFailed to add relay.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (is_number) {
|
||||
int idx = atoi(trimmed) - 1;
|
||||
if (idx >= 0 && idx < all_count) {
|
||||
relay_enabled[idx] = !relay_enabled[idx];
|
||||
} else {
|
||||
fprintf(stderr, "%sInvalid relay number (1-%d).%s\n", ANSI_RED, all_count, ANSI_RESET);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == 'r') {
|
||||
if (cfg->relay_count <= 1) {
|
||||
fprintf(stderr, "%sCannot remove last relay.%s\n", ANSI_RED, ANSI_RESET);
|
||||
|
||||
char c = (char)tolower((unsigned char)trimmed[0]);
|
||||
|
||||
if (c == 'q') {
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return -1;
|
||||
}
|
||||
if (c == 'b') {
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return 1;
|
||||
}
|
||||
if (c == 'd') {
|
||||
/* Count enabled relays. */
|
||||
int enabled_count = 0;
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
if (relay_enabled[i]) enabled_count++;
|
||||
}
|
||||
if (enabled_count <= 0) {
|
||||
fprintf(stderr, "%sAt least one relay must be enabled.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
if (read_line_prompt("Remove which number? ", input, sizeof(input)) != 0) return -1;
|
||||
if (line_is_quit(input)) return -1;
|
||||
int idx = atoi(input) - 1;
|
||||
if (relay_remove_index(cfg, idx) != 0) {
|
||||
fprintf(stderr, "%sInvalid relay number.%s\n", ANSI_RED, ANSI_RESET);
|
||||
/* Rebuild cfg->relays from enabled entries. */
|
||||
free_relays_only(cfg);
|
||||
cfg->relays = (char**)calloc((size_t)enabled_count, sizeof(char*));
|
||||
if (!cfg->relays) {
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return -1;
|
||||
}
|
||||
int idx = 0;
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
if (relay_enabled[i]) {
|
||||
cfg->relays[idx] = strdup(all_relays[i]);
|
||||
if (!cfg->relays[idx]) {
|
||||
free_relays_only(cfg);
|
||||
for (int j = 0; j < all_count; j++) free(all_relays[j]);
|
||||
return -1;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
cfg->relay_count = enabled_count;
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return 0;
|
||||
}
|
||||
if (c == 'a') {
|
||||
if (read_line_prompt(" Relay URL (wss:// or ws://): ", input, sizeof(input)) != 0) {
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return -1;
|
||||
}
|
||||
if (line_is_quit(input)) {
|
||||
for (int i = 0; i < all_count; i++) free(all_relays[i]);
|
||||
return -1;
|
||||
}
|
||||
/* Trim. */
|
||||
trimmed = input;
|
||||
while (*trimmed == ' ') trimmed++;
|
||||
if (trimmed[0] == '\0') continue;
|
||||
if (!(strncmp(trimmed, "wss://", 6) == 0 || strncmp(trimmed, "ws://", 5) == 0)) {
|
||||
fprintf(stderr, "%sRelay must start with wss:// or ws://%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
/* Check if already in the list. */
|
||||
int found = 0;
|
||||
for (int i = 0; i < all_count; i++) {
|
||||
if (all_relays[i] && strcmp(all_relays[i], trimmed) == 0) {
|
||||
relay_enabled[i] = 1;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (all_count >= RELAY_TOGGLE_MAX) {
|
||||
fprintf(stderr, "%sMaximum relay count reached.%s\n", ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
all_relays[all_count] = strdup(trimmed);
|
||||
relay_enabled[all_count] = 1;
|
||||
all_count++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
return prompt_relay_configuration_with_header(cfg, "Step 6 of 7", "New Agent Setup -- Relay Configuration");
|
||||
}
|
||||
|
||||
static int run_command_local(char* const argv[]) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
@@ -2096,19 +2219,21 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c
|
||||
|
||||
/* Build the key/signer argument tail for ExecStart.
|
||||
* - local mode: embeds the nsec directly (legacy behavior).
|
||||
* - nsigner_unix mode: passes --signer/--signer-socket/--signer-role/--signer-timeout
|
||||
* so the service process reconstructs the remote signer without holding the nsec.
|
||||
* - nsigner_tcp mode: passes --signer-tcp host:port plus role/timeout.
|
||||
* - nsigner_serial mode: passes --signer-serial <device> plus role/timeout.
|
||||
* - nsigner_qrexec mode: passes --signer-qrexec <target_qube> plus role/timeout.
|
||||
* - nsigner_unix mode: passes --signer/--signer-socket/--signer-role/--signer-path/
|
||||
* --signer-timeout so the service process reconstructs the remote signer without
|
||||
* holding the nsec.
|
||||
* - nsigner_tcp mode: passes --signer-tcp host:port plus role/path/timeout.
|
||||
* - nsigner_serial mode: passes --signer-serial <device> plus role/path/timeout.
|
||||
* - nsigner_qrexec mode: passes --signer-qrexec <target_qube> plus role/path/timeout.
|
||||
* - nsigner_fds mode: NOT installable as a systemd service (fds are
|
||||
* runtime-only); rejected by the caller before reaching here.
|
||||
* The agent pubkey, admin, LLM and relays are recovered from Nostr at boot. */
|
||||
char key_args[768] = {0};
|
||||
/* Build the optional --signer-index tail once; appended to all remote modes. */
|
||||
char index_tail[32] = {0};
|
||||
if (cfg->signer.nostr_index >= 0) {
|
||||
snprintf(index_tail, sizeof(index_tail), " --signer-index %d", cfg->signer.nostr_index);
|
||||
char key_args[1024] = {0};
|
||||
/* Build the --signer-path tail once; appended to all remote modes. The
|
||||
* path is single-quoted to survive the BIP-44 hardened markers ('). */
|
||||
char path_tail[OW_MAX_URL_LEN + 32] = {0};
|
||||
if (cfg->signer.role_path[0] != '\0') {
|
||||
snprintf(path_tail, sizeof(path_tail), " --signer-path '%s'", cfg->signer.role_path);
|
||||
}
|
||||
|
||||
if (strcmp(cfg->signer.mode, "nsigner_unix") == 0) {
|
||||
@@ -2118,7 +2243,7 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c
|
||||
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000,
|
||||
cfg->signer.socket_name[0] ? " --signer-socket " : "",
|
||||
cfg->signer.socket_name[0] ? cfg->signer.socket_name : "",
|
||||
index_tail);
|
||||
path_tail);
|
||||
} else if (strcmp(cfg->signer.mode, "nsigner_tcp") == 0) {
|
||||
char host_port[OW_MAX_SIGNER_HOST_LEN + 16] = {0};
|
||||
snprintf(host_port, sizeof(host_port), "%s:%d", cfg->signer.tcp_host, cfg->signer.tcp_port);
|
||||
@@ -2127,14 +2252,14 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c
|
||||
host_port,
|
||||
cfg->signer.role[0] ? cfg->signer.role : "main",
|
||||
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000,
|
||||
index_tail);
|
||||
path_tail);
|
||||
} else if (strcmp(cfg->signer.mode, "nsigner_serial") == 0) {
|
||||
snprintf(key_args, sizeof(key_args),
|
||||
"--signer nsigner_serial --signer-serial %s --signer-role %s --signer-timeout %d%s",
|
||||
cfg->signer.serial_device,
|
||||
cfg->signer.role[0] ? cfg->signer.role : "main",
|
||||
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000,
|
||||
index_tail);
|
||||
path_tail);
|
||||
} else if (strcmp(cfg->signer.mode, "nsigner_qrexec") == 0) {
|
||||
const char* svc = cfg->signer.service_name[0] ? cfg->signer.service_name : "qubes.NsignerRpc";
|
||||
snprintf(key_args, sizeof(key_args),
|
||||
@@ -2143,7 +2268,7 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c
|
||||
svc,
|
||||
cfg->signer.role[0] ? cfg->signer.role : "main",
|
||||
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000,
|
||||
index_tail);
|
||||
path_tail);
|
||||
} else if (strcmp(cfg->signer.mode, "nsigner_fds") == 0) {
|
||||
/* Defensive: callers reject this before install, but guard anyway. */
|
||||
fprintf(stderr, "%snsigner_fds mode cannot be installed as a systemd service "
|
||||
@@ -2453,10 +2578,43 @@ static int prompt_signer_transport(didactyl_config_t* cfg, nostr_signer_t** sign
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Common: key index + timeout. The key index selects which managed
|
||||
* key n_signer uses (default 0). Role is left empty since the
|
||||
* nostr_signer qube addresses keys by index. */
|
||||
cfg->signer.role[0] = '\0';
|
||||
/* Common: role + role_path + timeout. n_signer now requires BOTH role
|
||||
* and role_path together for nostr_* verbs — role-only is rejected
|
||||
* with 2009 path_required, bare nostr_index with 2006 nostr_index_deprecated. */
|
||||
char role_buf[WIZARD_LINE_MAX] = {0};
|
||||
if (read_line_prompt(" Role [nostr_range]: ", role_buf, sizeof(role_buf)) != 0) return -1;
|
||||
if (line_is_quit(role_buf)) return -1;
|
||||
snprintf(cfg->signer.role, sizeof(cfg->signer.role), "%s", role_buf[0] ? role_buf : "nostr_range");
|
||||
|
||||
fprintf(stderr, " Derivation path presets:\n"
|
||||
" 1) m/44'/1237'/0'/0/0 (NIP-06 standard)\n"
|
||||
" 2) m/44'/1237'/0'/0'/0' (all-hardened)\n"
|
||||
" 3) custom\n");
|
||||
char path_choice[8] = {0};
|
||||
if (read_line_prompt(" Select preset [1]: ", path_choice, sizeof(path_choice)) != 0) return -1;
|
||||
if (line_is_quit(path_choice)) return -1;
|
||||
const char* default_path = "m/44'/1237'/0'/0/0";
|
||||
if (path_choice[0] == '2') {
|
||||
default_path = "m/44'/1237'/0'/0'/0'";
|
||||
} else if (path_choice[0] == '3') {
|
||||
default_path = "";
|
||||
}
|
||||
char path_buf[WIZARD_LINE_MAX] = {0};
|
||||
if (default_path[0] != '\0') {
|
||||
char prompt[128] = {0};
|
||||
snprintf(prompt, sizeof(prompt), " role_path [%s]: ", default_path);
|
||||
if (read_line_prompt(prompt, path_buf, sizeof(path_buf)) != 0) return -1;
|
||||
} else {
|
||||
if (read_line_prompt(" role_path: ", path_buf, sizeof(path_buf)) != 0) return -1;
|
||||
}
|
||||
if (line_is_quit(path_buf)) return -1;
|
||||
snprintf(cfg->signer.role_path, sizeof(cfg->signer.role_path), "%s",
|
||||
path_buf[0] ? path_buf : default_path);
|
||||
if (cfg->signer.role_path[0] == '\0') {
|
||||
fprintf(stderr, "%srole_path is required (n_signer rejects role-only with 2009 path_required).%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
continue;
|
||||
}
|
||||
|
||||
char timeout_buf[32] = {0};
|
||||
if (read_line_prompt(" Timeout ms [15000]: ", timeout_buf, sizeof(timeout_buf)) != 0) return -1;
|
||||
@@ -2464,23 +2622,15 @@ static int prompt_signer_transport(didactyl_config_t* cfg, nostr_signer_t** sign
|
||||
cfg->signer.timeout_ms = (timeout_buf[0] != '\0') ? atoi(timeout_buf) : 15000;
|
||||
if (cfg->signer.timeout_ms <= 0) cfg->signer.timeout_ms = 15000;
|
||||
|
||||
char index_buf[32] = {0};
|
||||
if (read_line_prompt(" Key index [0]: ", index_buf, sizeof(index_buf)) != 0) return -1;
|
||||
if (line_is_quit(index_buf)) return -1;
|
||||
cfg->signer.nostr_index = (index_buf[0] != '\0') ? atoi(index_buf) : 0;
|
||||
if (cfg->signer.nostr_index < 0) {
|
||||
fprintf(stderr, "%sIndex must be >= 0; using 0.%s\n", ANSI_RED, ANSI_RESET);
|
||||
cfg->signer.nostr_index = 0;
|
||||
}
|
||||
|
||||
snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", chosen_mode);
|
||||
|
||||
/* Connectivity check. */
|
||||
fprintf(stderr, " Checking connectivity to n_signer (mode=%s, target=%s, service=%s, index=%d)...\n",
|
||||
fprintf(stderr, " Checking connectivity to n_signer (mode=%s, target=%s, service=%s, role=%s, path=%s)...\n",
|
||||
chosen_mode,
|
||||
cfg->signer.target_qube[0] ? cfg->signer.target_qube : "<n/a>",
|
||||
cfg->signer.service_name[0] ? cfg->signer.service_name : "<n/a>",
|
||||
cfg->signer.nostr_index);
|
||||
cfg->signer.role,
|
||||
cfg->signer.role_path);
|
||||
fflush(stderr);
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
nostr_signer_t* signer = NULL;
|
||||
@@ -2508,9 +2658,10 @@ static int prompt_signer_transport(didactyl_config_t* cfg, nostr_signer_t** sign
|
||||
cfg->signer.role, cfg->signer.timeout_ms);
|
||||
}
|
||||
|
||||
/* Apply nostr_index selector when set (overrides role on the n_signer side). */
|
||||
if (signer && cfg->signer.nostr_index >= 0) {
|
||||
(void)nostr_signer_nsigner_set_nostr_index(signer, cfg->signer.nostr_index);
|
||||
/* Apply role_path selector (required for all nsigner_* modes — n_signer
|
||||
* rejects role-only with 2009 path_required). */
|
||||
if (signer && cfg->signer.role_path[0] != '\0') {
|
||||
(void)nostr_signer_nsigner_set_role_path(signer, cfg->signer.role_path);
|
||||
}
|
||||
|
||||
if (!signer) {
|
||||
@@ -2524,10 +2675,14 @@ static int prompt_signer_transport(didactyl_config_t* cfg, nostr_signer_t** sign
|
||||
|
||||
char pubkey_hex[65] = {0};
|
||||
if (nostr_signer_get_public_key(signer, pubkey_hex) != 0) {
|
||||
const char* last_err = nostr_signer_last_error(signer);
|
||||
fprintf(stderr, "%sFailed to retrieve public key from n_signer (mode=%s).%s\n",
|
||||
ANSI_RED, chosen_mode, ANSI_RESET);
|
||||
fprintf(stderr, " Is the n_signer process running and reachable? "
|
||||
"For qrexec: is qrexec-client-vm available and the dom0 policy installed?\n");
|
||||
if (last_err && last_err[0] != '\0') {
|
||||
fprintf(stderr, " n_signer error: %s\n", last_err);
|
||||
}
|
||||
nostr_signer_free(signer);
|
||||
char pause_buf[WIZARD_LINE_MAX] = {0};
|
||||
(void)read_line_prompt(" Press Enter to try again (or q to quit): ", pause_buf, sizeof(pause_buf));
|
||||
@@ -2588,9 +2743,14 @@ static nostr_signer_t* wizard_construct_ephemeral_signer(const didactyl_config_t
|
||||
s = nostr_signer_nsigner_qrexec(cfg->signer.target_qube, svc,
|
||||
cfg->signer.role, cfg->signer.timeout_ms);
|
||||
}
|
||||
/* Apply nostr_index selector when set (overrides role on the n_signer side). */
|
||||
if (s && cfg->signer.nostr_index >= 0) {
|
||||
(void)nostr_signer_nsigner_set_nostr_index(s, cfg->signer.nostr_index);
|
||||
/* Apply role_path selector (required for all nsigner_* modes — n_signer
|
||||
* rejects role-only with 2009 path_required). */
|
||||
if (s && cfg->signer.role_path[0] != '\0') {
|
||||
(void)nostr_signer_nsigner_set_role_path(s, cfg->signer.role_path);
|
||||
}
|
||||
/* Apply derive_index for the algorithm-based derive (HMAC) verb. */
|
||||
if (s && cfg->signer.derive_index >= 0) {
|
||||
(void)nostr_signer_nsigner_set_derive_index(s, cfg->signer.derive_index);
|
||||
}
|
||||
return s;
|
||||
#else
|
||||
@@ -2601,7 +2761,7 @@ static nostr_signer_t* wizard_construct_ephemeral_signer(const didactyl_config_t
|
||||
|
||||
static int new_agent_identity_step(didactyl_config_t* cfg) {
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Identity");
|
||||
render_wizard_page_header("Step 3 of 8", "New Agent Setup -- Identity");
|
||||
print_option('g', "enerate a new Nostr keypair");
|
||||
print_option('p', "rovide an existing nsec");
|
||||
print_option('s', "ign with a running n_signer");
|
||||
@@ -2673,65 +2833,238 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
(void)genesis_path_out;
|
||||
(void)genesis_path_out_size;
|
||||
char agent_name[OW_MAX_NAME_LEN] = {0};
|
||||
render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Agent Profile");
|
||||
int rc;
|
||||
/* When the operator chooses a remote signer, this handle stays alive for
|
||||
* the duration of the flow so kind-30078 NIP-44 decrypt/encrypt is routed
|
||||
* through n_signer (no nsec in the agent process). Freed before return. */
|
||||
nostr_signer_t* flow_signer = NULL;
|
||||
|
||||
/* Step 1 of 8 — Agent Profile */
|
||||
render_wizard_page_header("Step 1 of 8", "New Agent Setup -- Agent Profile");
|
||||
if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
|
||||
if (line_is_quit(agent_name)) return -1;
|
||||
if (agent_name[0] == '\0') {
|
||||
snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
|
||||
}
|
||||
|
||||
int rc = new_agent_identity_step(cfg);
|
||||
/* Step 2 of 8 — Relay Configuration (before identity, so the operator
|
||||
* can adjust relays before the validation step queries the network). */
|
||||
for (;;) {
|
||||
rc = prompt_relay_configuration_with_header(cfg, "Step 2 of 8", "New Agent Setup -- Relay Configuration");
|
||||
if (rc < 0) return -1;
|
||||
if (rc == 0 || rc == 1) break;
|
||||
}
|
||||
|
||||
/* Step 3 of 8 — Identity */
|
||||
rc = new_agent_identity_step(cfg);
|
||||
if (rc != 0) return rc < 0 ? -1 : 1;
|
||||
|
||||
if (prompt_admin_pubkey(cfg) != 0) return -1;
|
||||
|
||||
/* Step 4 of 8 — Identity Validation & Existing Agent Detection */
|
||||
int exists = 0;
|
||||
int admin_name_found = 0;
|
||||
char admin_name[128] = {0};
|
||||
if (validate_new_agent_identity_and_admin(cfg, &exists, admin_name, sizeof(admin_name), &admin_name_found) != 0) {
|
||||
fprintf(stderr, "%sWarning: Unable to validate identity/admin against Nostr right now.%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
} else {
|
||||
render_wizard_page_header("Step 4 of 7", "New Agent Setup -- Identity Validation");
|
||||
fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup");
|
||||
render_wizard_page_header("Step 4 of 8", "New Agent Setup -- Identity Validation");
|
||||
fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND" : "NOT FOUND (new identity available)");
|
||||
if (admin_name_found) {
|
||||
fprintf(stderr, " Admin profile: %s\n", admin_name);
|
||||
} else {
|
||||
fprintf(stderr, " Admin profile: NOT FOUND (you can still continue)\n");
|
||||
fprintf(stderr, " Admin profile: N/A\n");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
if (exists) {
|
||||
print_option('c', "ontinue anyway");
|
||||
print_option('a', "bort");
|
||||
wizard_option_t confirm[] = {{'c', ""}, {'a', ""}};
|
||||
char cc = read_menu_choice(confirm, 2);
|
||||
if (cc != 'c') {
|
||||
return -1;
|
||||
fprintf(stderr, " This identity already has an agent configured on the network.\n\n");
|
||||
print_option('d', "ownload existing config and review it");
|
||||
print_option('r', "estart with a fresh identity");
|
||||
print_option('c', "ontinue anyway (overwrite existing state)");
|
||||
print_option('q', "uit");
|
||||
wizard_option_t confirm[] = {{'d', ""}, {'r', ""}, {'c', ""}, {'q', ""}};
|
||||
char cc = read_menu_choice(confirm, 4);
|
||||
if (cc == 'q') return -1;
|
||||
if (cc == 'r') {
|
||||
/* Restart: reset config and go back to identity step. */
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
flow_signer = NULL;
|
||||
config_free(cfg);
|
||||
config_set_defaults(cfg);
|
||||
if (set_default_relays(cfg) != 0) return -1;
|
||||
return 2; /* signals main loop to restart new_agent_flow */
|
||||
}
|
||||
if (cc == 'd') {
|
||||
/* Download existing config and present it for review. */
|
||||
int agent_name_found = 0;
|
||||
char recovered_name[OW_MAX_NAME_LEN] = {0};
|
||||
if (recover_full_config_from_nostr(cfg, flow_signer, recovered_name, sizeof(recovered_name), &agent_name_found) != 0) {
|
||||
fprintf(stderr, "%sWarning: unable to recover full config from Nostr, continuing with partial recovery.%s\n",
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
}
|
||||
if (agent_name_found && recovered_name[0] != '\0') {
|
||||
snprintf(agent_name, sizeof(agent_name), "%s", recovered_name);
|
||||
}
|
||||
|
||||
/* Show downloaded config on a single review page. */
|
||||
for (;;) {
|
||||
char masked_key[64] = {0};
|
||||
size_t api_len = strlen(cfg->llm.api_key);
|
||||
if (api_len >= 8U) {
|
||||
snprintf(masked_key, sizeof(masked_key), "%.4s...%s",
|
||||
cfg->llm.api_key, cfg->llm.api_key + api_len - 4U);
|
||||
} else if (api_len > 0U) {
|
||||
snprintf(masked_key, sizeof(masked_key), "****");
|
||||
} else {
|
||||
snprintf(masked_key, sizeof(masked_key), "(not set)");
|
||||
}
|
||||
|
||||
render_wizard_page_header("Step 4 of 8", "Downloaded Configuration");
|
||||
fprintf(stderr, " Name: %s\n", agent_name);
|
||||
fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex);
|
||||
fprintf(stderr, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)");
|
||||
fprintf(stderr, " LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)");
|
||||
fprintf(stderr, " LLM Model: %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)");
|
||||
fprintf(stderr, " LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)");
|
||||
fprintf(stderr, " LLM API Key: %s\n", masked_key);
|
||||
fprintf(stderr, " Relays: %d configured\n", cfg->relay_count);
|
||||
fprintf(stderr, "\n");
|
||||
print_option('e', "dit these settings");
|
||||
print_option('b', "oot with this config now");
|
||||
print_option('i', "nstall dedicated-user systemd service and boot");
|
||||
print_option('q', "uit");
|
||||
wizard_option_t review_opts[] = {{'e', ""}, {'b', ""}, {'i', ""}, {'q', ""}};
|
||||
char rc2 = read_menu_choice(review_opts, 4);
|
||||
if (rc2 == 'q') return -1;
|
||||
if (rc2 == 'e') {
|
||||
/* Enter the edit loop: admin, LLM, relays. */
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 4 of 8", "Edit Downloaded Configuration");
|
||||
fprintf(stderr, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)");
|
||||
fprintf(stderr, " LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)");
|
||||
fprintf(stderr, " LLM Model: %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)");
|
||||
fprintf(stderr, " LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)");
|
||||
fprintf(stderr, " LLM API Key: %s\n", masked_key);
|
||||
fprintf(stderr, " Relays: %d configured\n", cfg->relay_count);
|
||||
fprintf(stderr, "\n");
|
||||
print_option('a', "dmin pubkey");
|
||||
print_option('l', "lm config");
|
||||
print_option('r', "elay configuration");
|
||||
print_option('d', "one -- return to review");
|
||||
print_option('q', "uit");
|
||||
wizard_option_t edit_opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'d', ""}, {'q', ""}};
|
||||
char ec = read_menu_choice(edit_opts, 5);
|
||||
if (ec == 'q') return -1;
|
||||
if (ec == 'd') break;
|
||||
if (ec == 'a') {
|
||||
if (prompt_admin_pubkey_with_header(cfg, "Step 4 of 8", "Edit -- Administrator") != 0) return -1;
|
||||
}
|
||||
if (ec == 'l') {
|
||||
if (prompt_llm_config_with_header(cfg, "Step 4 of 8", "Edit -- LLM Provider") != 0) return -1;
|
||||
}
|
||||
if (ec == 'r') {
|
||||
for (;;) {
|
||||
int rrc = prompt_relay_configuration_with_header(cfg, "Step 4 of 8", "Edit -- Relay Configuration");
|
||||
if (rrc < 0) return -1;
|
||||
if (rrc == 1) break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue; /* back to review page */
|
||||
}
|
||||
if (rc2 == 'b' || rc2 == 'i') {
|
||||
/* Boot or install with the downloaded config.
|
||||
* Skip Steps 5-8 since the config is already complete. */
|
||||
if (configure_default_skills_for_agent(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
if (apply_default_startup_events(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
if (rc2 == 'b') {
|
||||
nostr_signer_t* pub_signer = wizard_construct_ephemeral_signer(cfg);
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg, pub_signer) != 0) {
|
||||
fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n",
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
}
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
fprintf(stderr, "%sBooting with downloaded config. Check your messages for initial greeting.%s\n",
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
return 0;
|
||||
}
|
||||
if (rc2 == 'i') {
|
||||
if (strcmp(cfg->signer.mode, "nsigner_fds") == 0) {
|
||||
fprintf(stderr, "%snsigner_fds mode cannot be installed as a systemd service.%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
nostr_signer_t* pub_signer = wizard_construct_ephemeral_signer(cfg);
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg, pub_signer) != 0) {
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
fprintf(stderr, "%sFailed to publish runtime config to Nostr; refusing install.%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
char service_user[64] = {0};
|
||||
sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
|
||||
char service_name[96] = {0};
|
||||
snprintf(service_name, sizeof(service_name), "%s.service", service_user);
|
||||
sleep(2);
|
||||
char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
|
||||
if (run_privileged_command_local(is_active_argv) == 0) {
|
||||
fprintf(stderr, "%sService %s is active. Setup complete.%s\n", ANSI_YELLOW, service_name, ANSI_RESET);
|
||||
} else {
|
||||
fprintf(stderr, "%sService installed but not active. Check: sudo systemctl status %s%s\n",
|
||||
ANSI_RED, service_name, ANSI_RESET);
|
||||
}
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* unreachable */
|
||||
}
|
||||
/* cc == 'c': continue anyway (overwrite), fall through to Steps 5-8 */
|
||||
} else {
|
||||
/* No existing agent found. */
|
||||
print_option('c', "ontinue to administrator setup");
|
||||
print_option('q', "uit");
|
||||
wizard_option_t confirm[] = {{'c', ""}, {'q', ""}};
|
||||
char cc = read_menu_choice(confirm, 2);
|
||||
if (cc != 'c') return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Step 5 of 8 — Administrator */
|
||||
if (prompt_admin_pubkey(cfg) != 0) return -1;
|
||||
|
||||
/* Step 6 of 8 — LLM Provider */
|
||||
if (prompt_llm_config(cfg) != 0) return -1;
|
||||
|
||||
for (;;) {
|
||||
rc = prompt_relay_configuration(cfg);
|
||||
if (rc < 0) return -1;
|
||||
if (rc == 0) break;
|
||||
if (prompt_llm_config(cfg) != 0) return -1;
|
||||
}
|
||||
|
||||
/* Step 7 of 8 — Default Skills */
|
||||
if (configure_default_skills_for_agent(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (apply_default_startup_events(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
render_wizard_page_header("Step 7 of 7", "New Agent Setup -- Review");
|
||||
/* Step 8 of 8 — Review */
|
||||
render_wizard_page_header("Step 8 of 8", "New Agent Setup -- Review");
|
||||
fprintf(stderr, " Name: %s\n", agent_name);
|
||||
fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex);
|
||||
fprintf(stderr, " Admin: %.16s...\n", cfg->admin.pubkey);
|
||||
@@ -2746,86 +3079,66 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
|
||||
wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 4);
|
||||
if (c == 'q') return -1;
|
||||
if (c == 's') return 2;
|
||||
if (c == 'q') {
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
if (c == 's') {
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return 2;
|
||||
}
|
||||
if (c == 'b') {
|
||||
/* Publish kind 30078 agent_config + llm_config so the --nsec-only
|
||||
restart path can recover them. Non-fatal: main() will also
|
||||
publish during bootstrap, but doing it here is belt-and-suspenders.
|
||||
In remote signer modes, construct an ephemeral signer so the
|
||||
kind-30078 NIP-44 encrypt is routed through n_signer (no nsec in
|
||||
the agent process). */
|
||||
nostr_signer_t* pub_signer = wizard_construct_ephemeral_signer(cfg);
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg, pub_signer) != 0) {
|
||||
fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
}
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
fprintf(stderr, "%sStep 8 of 8 -- Booting new agent. Check your messages for initial greeting.%s\n",
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (c == 'i') {
|
||||
/* nsigner_fds cannot be installed as a systemd service (fds are
|
||||
* runtime-only and cannot be embedded in ExecStart). */
|
||||
if (strcmp(cfg->signer.mode, "nsigner_fds") == 0) {
|
||||
fprintf(stderr,
|
||||
"%snsigner_fds mode cannot be installed as a systemd service (file descriptors are "
|
||||
"runtime-only and cannot be embedded in ExecStart). Choose 'boot now' or wire the "
|
||||
"fd-passing yourself via a wrapper unit.%s\n",
|
||||
fprintf(stderr, "%snsigner_fds mode cannot be installed as a systemd service.%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Publish kind 30078 agent_config + llm_config BEFORE installing the
|
||||
service. The systemd service starts with --nsec only and depends
|
||||
on recovering these from relays. */
|
||||
nostr_signer_t* pub_signer = wizard_construct_ephemeral_signer(cfg);
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg, pub_signer) != 0) {
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
fprintf(stderr,
|
||||
"%sFailed to publish runtime config to Nostr; refusing install to avoid missing admin/LLM on first boot.%s\n",
|
||||
ANSI_RED,
|
||||
ANSI_RESET);
|
||||
fprintf(stderr, "%sFailed to publish runtime config to Nostr; refusing install.%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
if (pub_signer) nostr_signer_free(pub_signer);
|
||||
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char service_user[64] = {0};
|
||||
sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
|
||||
|
||||
char service_name[96] = {0};
|
||||
snprintf(service_name, sizeof(service_name), "%s.service", service_user);
|
||||
|
||||
sleep(2);
|
||||
char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
|
||||
if (run_privileged_command_local(is_active_argv) == 0) {
|
||||
fprintf(stderr,
|
||||
"%sStep 7 of 7 -- Service %s is active. Setup is complete; this wizard will now exit. "
|
||||
"Expect a startup message in the admin inbox.%s\n",
|
||||
ANSI_YELLOW,
|
||||
service_name,
|
||||
ANSI_RESET);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"%sStep 7 of 7 -- Service installed but not active yet. This wizard will now exit. "
|
||||
"Check status with: sudo systemctl status %s ; logs with: sudo journalctl -u %s -f%s\n",
|
||||
ANSI_RED,
|
||||
service_name,
|
||||
service_name,
|
||||
ANSI_RESET);
|
||||
}
|
||||
|
||||
return 1;
|
||||
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
char service_user[64] = {0};
|
||||
sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
|
||||
char service_name[96] = {0};
|
||||
snprintf(service_name, sizeof(service_name), "%s.service", service_user);
|
||||
sleep(2);
|
||||
char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
|
||||
if (run_privileged_command_local(is_active_argv) == 0) {
|
||||
fprintf(stderr, "%sStep 8 of 8 -- Service %s is active. Setup complete.%s\n",
|
||||
ANSI_YELLOW, service_name, ANSI_RESET);
|
||||
} else {
|
||||
fprintf(stderr, "%sStep 8 of 8 -- Service installed but not active. Check: sudo systemctl status %s%s\n",
|
||||
ANSI_RED, service_name, ANSI_RESET);
|
||||
}
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (flow_signer) nostr_signer_free(flow_signer);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -7,6 +7,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
@@ -46,7 +47,12 @@ static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext,
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
plain,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[config] nip44_encrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_ciphertext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
@@ -80,7 +86,12 @@ static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
ciphertext,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[config] nip44_decrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_plaintext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
|
||||
+13
-2
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
@@ -68,7 +69,12 @@ static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext,
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
plain,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[memory] nip44_encrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_ciphertext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
@@ -102,7 +108,12 @@ static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
ciphertext,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[memory] nip44_decrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_plaintext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
@@ -93,8 +94,13 @@ char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json) {
|
||||
plaintext->valuestring,
|
||||
&ciphertext);
|
||||
if (rc != NOSTR_SUCCESS || !ciphertext) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
char err_buf[256] = {0};
|
||||
snprintf(err_buf, sizeof(err_buf), "nostr_encrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
DEBUG_ERROR("[nostr_dm] %s", err_buf);
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("nostr_encrypt failed");
|
||||
return json_error_local(err_buf);
|
||||
}
|
||||
} else {
|
||||
unsigned char recipient_pubkey[32];
|
||||
@@ -162,8 +168,13 @@ char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json) {
|
||||
ciphertext->valuestring,
|
||||
&plaintext);
|
||||
if (rc != NOSTR_SUCCESS || !plaintext) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
char err_buf[256] = {0};
|
||||
snprintf(err_buf, sizeof(err_buf), "nostr_decrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
DEBUG_ERROR("[nostr_dm] %s", err_buf);
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("nostr_decrypt failed");
|
||||
return json_error_local(err_buf);
|
||||
}
|
||||
} else {
|
||||
unsigned char sender_pubkey[32];
|
||||
|
||||
@@ -631,6 +631,16 @@ char* execute_nostr_post(const char* args_json) {
|
||||
return json_error_local("nostr_post tags must be an array when provided");
|
||||
}
|
||||
|
||||
/* Optional PoW (NIP-13) mining args. When difficulty > 0 and a remote
|
||||
* signer is available, route through nostr_signer_mine_event instead of
|
||||
* the normal sign path. */
|
||||
cJSON* difficulty_json = cJSON_GetObjectItemCaseSensitive(args, "difficulty");
|
||||
int difficulty = (difficulty_json && cJSON_IsNumber(difficulty_json)) ? (int)difficulty_json->valuedouble : 0;
|
||||
cJSON* threads_json = cJSON_GetObjectItemCaseSensitive(args, "threads");
|
||||
int threads = (threads_json && cJSON_IsNumber(threads_json)) ? (int)threads_json->valuedouble : 1;
|
||||
cJSON* timeout_json = cJSON_GetObjectItemCaseSensitive(args, "timeout_sec");
|
||||
int timeout_sec = (timeout_json && cJSON_IsNumber(timeout_json)) ? (int)timeout_json->valuedouble : 600;
|
||||
|
||||
cJSON* tags_dup = NULL;
|
||||
if (tags) {
|
||||
tags_dup = cJSON_Duplicate(tags, 1);
|
||||
@@ -646,10 +656,59 @@ char* execute_nostr_post(const char* args_json) {
|
||||
nostr_publish_result_t publish_result;
|
||||
memset(&publish_result, 0, sizeof(publish_result));
|
||||
|
||||
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
|
||||
int rc;
|
||||
if (difficulty > 0) {
|
||||
/* PoW path: mine event through the remote signer, then publish. */
|
||||
nostr_signer_t* signer = nostr_handler_get_signer();
|
||||
if (!signer) {
|
||||
cJSON_Delete(tags_dup);
|
||||
cJSON_Delete(args);
|
||||
free(repaired_args_json);
|
||||
return json_error_local("nostr_post PoW requires a remote n_signer "
|
||||
"(local mode does not support mine_event)");
|
||||
}
|
||||
/* Build the unsigned event JSON. */
|
||||
cJSON* unsigned_event = cJSON_CreateObject();
|
||||
if (!unsigned_event) {
|
||||
cJSON_Delete(tags_dup);
|
||||
cJSON_Delete(args);
|
||||
free(repaired_args_json);
|
||||
return json_error_local("nostr_post PoW: memory allocation failed");
|
||||
}
|
||||
cJSON_AddNumberToObject(unsigned_event, "kind", kind->valuedouble);
|
||||
cJSON_AddStringToObject(unsigned_event, "content", content->valuestring);
|
||||
if (tags_dup) {
|
||||
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_Duplicate(tags_dup, 1));
|
||||
} else {
|
||||
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_CreateArray());
|
||||
}
|
||||
/* created_at is set by the signer. */
|
||||
|
||||
cJSON* mined_event = NULL;
|
||||
int mine_rc = nostr_signer_mine_event(signer, unsigned_event,
|
||||
difficulty, timeout_sec, threads,
|
||||
&mined_event);
|
||||
cJSON_Delete(unsigned_event);
|
||||
if (mine_rc != NOSTR_SUCCESS || !mined_event) {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_Delete(tags_dup);
|
||||
cJSON_Delete(args);
|
||||
free(repaired_args_json);
|
||||
char err_buf[256] = {0};
|
||||
snprintf(err_buf, sizeof(err_buf), "nostr_post PoW mining failed (rc=%d): %s",
|
||||
mine_rc, (le && le[0]) ? le : "<no detail>");
|
||||
return json_error_local(err_buf);
|
||||
}
|
||||
/* Publish the mined+signed event through the relay pool. */
|
||||
rc = nostr_handler_publish_event(mined_event, &publish_result);
|
||||
cJSON_Delete(mined_event);
|
||||
} else {
|
||||
/* Normal (non-PoW) path. */
|
||||
rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
|
||||
content->valuestring,
|
||||
tags_dup,
|
||||
&publish_result);
|
||||
}
|
||||
cJSON_Delete(tags_dup);
|
||||
cJSON_Delete(args);
|
||||
free(repaired_args_json);
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "tools_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NULL;
|
||||
cJSON_AddBoolToObject(root, "success", 0);
|
||||
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
|
||||
char* out = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Unified signer_crypto tool — exposes all algorithm-based n_signer verbs
|
||||
* through a single tool with an "operation" enum. Requires a remote n_signer;
|
||||
* returns a clear error in local mode since the local backend does not support
|
||||
* algorithm-based verbs (PQ crypto, OTP, etc.). */
|
||||
char* execute_signer_crypto(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx) return json_error_local("tool context unavailable");
|
||||
|
||||
nostr_signer_t* signer = ctx->signer;
|
||||
if (!signer) {
|
||||
/* Fall back to the process-lifetime signer if the tools context
|
||||
* doesn't have one wired (e.g. during startup). */
|
||||
signer = nostr_handler_get_signer();
|
||||
}
|
||||
if (!signer) {
|
||||
return json_error_local("signer_crypto requires a remote n_signer "
|
||||
"(local mode does not support algorithm-based verbs)");
|
||||
}
|
||||
|
||||
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* op_json = cJSON_GetObjectItemCaseSensitive(args, "operation");
|
||||
if (!op_json || !cJSON_IsString(op_json) || !op_json->valuestring || op_json->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("signer_crypto requires a string 'operation' field");
|
||||
}
|
||||
const char* operation = op_json->valuestring;
|
||||
|
||||
cJSON* algorithm = cJSON_GetObjectItemCaseSensitive(args, "algorithm");
|
||||
const char* alg_str = (algorithm && cJSON_IsString(algorithm) && algorithm->valuestring)
|
||||
? algorithm->valuestring : NULL;
|
||||
cJSON* index_json = cJSON_GetObjectItemCaseSensitive(args, "index");
|
||||
int index = (index_json && cJSON_IsNumber(index_json)) ? (int)index_json->valuedouble : 0;
|
||||
cJSON* scheme_json = cJSON_GetObjectItemCaseSensitive(args, "scheme");
|
||||
const char* scheme = (scheme_json && cJSON_IsString(scheme_json) && scheme_json->valuestring)
|
||||
? scheme_json->valuestring : NULL;
|
||||
|
||||
cJSON* result = cJSON_CreateObject();
|
||||
if (!result) { cJSON_Delete(args); return NULL; }
|
||||
cJSON_AddBoolToObject(result, "success", 1);
|
||||
cJSON_AddStringToObject(result, "operation", operation);
|
||||
|
||||
int rc;
|
||||
char* result_str = NULL;
|
||||
cJSON* info_out = NULL;
|
||||
int valid_out = 0;
|
||||
|
||||
if (strcmp(operation, "get_info") == 0) {
|
||||
rc = nostr_signer_get_info(signer, &info_out);
|
||||
if (rc == NOSTR_SUCCESS && info_out) {
|
||||
char* info_json = cJSON_PrintUnformatted(info_out);
|
||||
cJSON_AddStringToObject(result, "info", info_json ? info_json : "{}");
|
||||
free(info_json);
|
||||
cJSON_Delete(info_out);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "get_info failed");
|
||||
}
|
||||
} else if (strcmp(operation, "get_public_key") == 0) {
|
||||
if (!alg_str) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "get_public_key requires 'algorithm' (e.g. secp256k1, ed25519, ml-dsa-65)");
|
||||
} else {
|
||||
rc = nostr_signer_get_public_key_alg(signer, alg_str, index, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "get_public_key failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "sign") == 0) {
|
||||
cJSON* msg_json = cJSON_GetObjectItemCaseSensitive(args, "message_hex");
|
||||
if (!msg_json || !cJSON_IsString(msg_json) || !msg_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "sign requires 'message_hex' (hex-encoded message bytes)");
|
||||
} else if (!alg_str) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "sign requires 'algorithm' (e.g. secp256k1, ed25519, ml-dsa-65, slh-dsa-128s)");
|
||||
} else {
|
||||
size_t msg_hex_len = strlen(msg_json->valuestring);
|
||||
size_t msg_bytes_len = msg_hex_len / 2;
|
||||
unsigned char* msg_bytes = (unsigned char*)malloc(msg_bytes_len + 1);
|
||||
if (!msg_bytes) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "memory allocation failed");
|
||||
} else if (nostr_hex_to_bytes(msg_json->valuestring, msg_bytes, (int)msg_bytes_len) != 0) {
|
||||
free(msg_bytes);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "invalid message_hex");
|
||||
} else {
|
||||
rc = nostr_signer_sign(signer, alg_str, index, scheme, msg_bytes, msg_bytes_len, &result_str);
|
||||
free(msg_bytes);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "sign failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "verify") == 0) {
|
||||
cJSON* msg_json = cJSON_GetObjectItemCaseSensitive(args, "message_hex");
|
||||
cJSON* sig_json = cJSON_GetObjectItemCaseSensitive(args, "signature_hex");
|
||||
if (!msg_json || !cJSON_IsString(msg_json) || !msg_json->valuestring ||
|
||||
!sig_json || !cJSON_IsString(sig_json) || !sig_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "verify requires 'message_hex' and 'signature_hex'");
|
||||
} else if (!alg_str) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "verify requires 'algorithm'");
|
||||
} else {
|
||||
size_t msg_hex_len = strlen(msg_json->valuestring);
|
||||
size_t msg_bytes_len = msg_hex_len / 2;
|
||||
unsigned char* msg_bytes = (unsigned char*)malloc(msg_bytes_len + 1);
|
||||
size_t sig_hex_len = strlen(sig_json->valuestring);
|
||||
size_t sig_bytes_len = sig_hex_len / 2;
|
||||
unsigned char* sig_bytes = (unsigned char*)malloc(sig_bytes_len + 1);
|
||||
if (!msg_bytes || !sig_bytes) {
|
||||
free(msg_bytes); free(sig_bytes);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "memory allocation failed");
|
||||
} else if (nostr_hex_to_bytes(msg_json->valuestring, msg_bytes, (int)msg_bytes_len) != 0 ||
|
||||
nostr_hex_to_bytes(sig_json->valuestring, sig_bytes, (int)sig_bytes_len) != 0) {
|
||||
free(msg_bytes); free(sig_bytes);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "invalid hex in message_hex or signature_hex");
|
||||
} else {
|
||||
rc = nostr_signer_verify(signer, alg_str, index, scheme,
|
||||
msg_bytes, msg_bytes_len,
|
||||
sig_bytes, sig_bytes_len,
|
||||
&valid_out);
|
||||
free(msg_bytes); free(sig_bytes);
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
cJSON_AddBoolToObject(result, "valid", valid_out);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "verify failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "encapsulate") == 0) {
|
||||
cJSON* peer_json = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey_hex");
|
||||
if (!peer_json || !cJSON_IsString(peer_json) || !peer_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "encapsulate requires 'peer_pubkey_hex' (ML-KEM-768 public key)");
|
||||
} else {
|
||||
rc = nostr_signer_encapsulate(signer, peer_json->valuestring, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "encapsulate failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "decapsulate") == 0) {
|
||||
cJSON* ct_json = cJSON_GetObjectItemCaseSensitive(args, "ciphertext_hex");
|
||||
if (!ct_json || !cJSON_IsString(ct_json) || !ct_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "decapsulate requires 'ciphertext_hex' and 'index'");
|
||||
} else {
|
||||
rc = nostr_signer_decapsulate(signer, index, ct_json->valuestring, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "decapsulate failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "derive_shared_secret") == 0) {
|
||||
cJSON* peer_json = cJSON_GetObjectItemCaseSensitive(args, "peer_pubkey_hex");
|
||||
if (!peer_json || !cJSON_IsString(peer_json) || !peer_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "derive_shared_secret requires 'peer_pubkey_hex' (X25519) and 'index'");
|
||||
} else {
|
||||
rc = nostr_signer_derive_shared_secret(signer, index, peer_json->valuestring, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "derive_shared_secret failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "derive_hmac") == 0) {
|
||||
cJSON* data_json = cJSON_GetObjectItemCaseSensitive(args, "data");
|
||||
if (!data_json || !cJSON_IsString(data_json) || !data_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "derive_hmac requires 'data' (UTF-8 string)");
|
||||
} else {
|
||||
rc = nostr_signer_derive_hmac(signer, data_json->valuestring, (char[65]){0});
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
char digest_hex[65] = {0};
|
||||
nostr_signer_derive_hmac(signer, data_json->valuestring, digest_hex);
|
||||
cJSON_AddStringToObject(result, "digest_hex", digest_hex);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "derive_hmac failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "otp_encrypt") == 0) {
|
||||
cJSON* pt_json = cJSON_GetObjectItemCaseSensitive(args, "plaintext_b64");
|
||||
cJSON* enc_json = cJSON_GetObjectItemCaseSensitive(args, "encoding");
|
||||
const char* encoding = (enc_json && cJSON_IsString(enc_json) && enc_json->valuestring)
|
||||
? enc_json->valuestring : NULL;
|
||||
if (!pt_json || !cJSON_IsString(pt_json) || !pt_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "otp_encrypt requires 'plaintext_b64' (base64-encoded plaintext)");
|
||||
} else {
|
||||
rc = nostr_signer_otp_encrypt(signer, pt_json->valuestring, encoding, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "otp_encrypt failed");
|
||||
}
|
||||
}
|
||||
} else if (strcmp(operation, "otp_decrypt") == 0) {
|
||||
cJSON* ct_json = cJSON_GetObjectItemCaseSensitive(args, "ciphertext");
|
||||
cJSON* enc_json = cJSON_GetObjectItemCaseSensitive(args, "encoding");
|
||||
const char* encoding = (enc_json && cJSON_IsString(enc_json) && enc_json->valuestring)
|
||||
? enc_json->valuestring : NULL;
|
||||
if (!ct_json || !cJSON_IsString(ct_json) || !ct_json->valuestring) {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "otp_decrypt requires 'ciphertext' (ASCII-armored or base64 blob)");
|
||||
} else {
|
||||
rc = nostr_signer_otp_decrypt(signer, ct_json->valuestring, encoding, &result_str);
|
||||
if (rc == NOSTR_SUCCESS && result_str) {
|
||||
cJSON_AddStringToObject(result, "result", result_str);
|
||||
free(result_str);
|
||||
} else {
|
||||
const char* le = nostr_signer_last_error(signer);
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", (le && le[0]) ? le : "otp_decrypt failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cJSON_AddBoolToObject(result, "success", 0);
|
||||
cJSON_AddStringToObject(result, "error", "unknown operation");
|
||||
}
|
||||
|
||||
cJSON_Delete(args);
|
||||
char* json = cJSON_PrintUnformatted(result);
|
||||
cJSON_Delete(result);
|
||||
return json;
|
||||
}
|
||||
+13
-2
@@ -8,6 +8,7 @@
|
||||
#include <time.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../debug.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
@@ -69,7 +70,12 @@ static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext,
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
plain,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[task] nip44_encrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_ciphertext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
@@ -103,7 +109,12 @@ static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext
|
||||
ctx->cfg->keys.public_key_hex,
|
||||
ciphertext,
|
||||
&signer_out);
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
||||
if (rc != NOSTR_SUCCESS || !signer_out) {
|
||||
const char* le = nostr_signer_last_error(ctx->signer);
|
||||
DEBUG_ERROR("[task] nip44_decrypt failed (rc=%d): %s",
|
||||
rc, (le && le[0]) ? le : "<no detail>");
|
||||
return -1;
|
||||
}
|
||||
*out_plaintext = signer_out;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
|
||||
if (strcmp(tool_name, "nostr_post") == 0) {
|
||||
return execute_nostr_post(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "signer_crypto") == 0) {
|
||||
return execute_signer_crypto(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "nostr_delete") == 0) {
|
||||
return execute_nostr_delete(args_json);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ char* execute_config_recall(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_adopted_skills(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_trigger_event(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_nostr_dm_history(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_signer_crypto(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
char* execute_cashu_wallet_balance(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_info(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
@@ -41,6 +41,21 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(p_tags_items, "items", p_tag_item);
|
||||
cJSON_AddItemToObject(p_tags, "items", p_tags_items);
|
||||
cJSON_AddItemToObject(t1_props, "tags", p_tags);
|
||||
/* Optional PoW (NIP-13) mining args. When difficulty > 0 and a remote
|
||||
* n_signer is available, the event is mined through the signer before
|
||||
* publishing. Requires --signer-path and --signer-role to be set. */
|
||||
cJSON* p_difficulty = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_difficulty, "type", "integer");
|
||||
cJSON_AddStringToObject(p_difficulty, "description", "NIP-13 PoW difficulty (leading zero bits). Requires a remote n_signer. Default: 0 (no mining)");
|
||||
cJSON_AddItemToObject(t1_props, "difficulty", p_difficulty);
|
||||
cJSON* p_threads = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_threads, "type", "integer");
|
||||
cJSON_AddStringToObject(p_threads, "description", "Mining threads (default: 1, max: 32). Only used when difficulty > 0");
|
||||
cJSON_AddItemToObject(t1_props, "threads", p_threads);
|
||||
cJSON* p_pow_timeout = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_pow_timeout, "type", "integer");
|
||||
cJSON_AddStringToObject(p_pow_timeout, "description", "Mining timeout in seconds (default: 600). Only used when difficulty > 0");
|
||||
cJSON_AddItemToObject(t1_props, "timeout_sec", p_pow_timeout);
|
||||
cJSON_AddItemToArray(t1_required, cJSON_CreateString("kind"));
|
||||
cJSON_AddItemToArray(t1_required, cJSON_CreateString("content"));
|
||||
|
||||
@@ -2244,6 +2259,96 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t67, "function", t67_fn);
|
||||
cJSON_AddItemToArray(tools, t67);
|
||||
|
||||
/* Tool 68: signer_crypto — unified access to all algorithm-based n_signer verbs. */
|
||||
cJSON* t68 = cJSON_CreateObject();
|
||||
cJSON* t68_fn = cJSON_CreateObject();
|
||||
cJSON* t68_params = cJSON_CreateObject();
|
||||
cJSON* t68_props = cJSON_CreateObject();
|
||||
cJSON* t68_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t68, "type", "function");
|
||||
cJSON_AddStringToObject(t68_fn, "name", "signer_crypto");
|
||||
cJSON_AddStringToObject(t68_fn, "description",
|
||||
"Access algorithm-based n_signer crypto operations. Requires a remote n_signer "
|
||||
"(local mode does not support algorithm verbs). Operations: get_info (signer metadata), "
|
||||
"get_public_key (algorithm+index), sign (message_hex), verify (message_hex+signature_hex), "
|
||||
"encapsulate (ML-KEM-768), decapsulate (ML-KEM-768), derive_shared_secret (X25519), "
|
||||
"derive_hmac (secp256k1 HMAC-SHA256), otp_encrypt, otp_decrypt. "
|
||||
"Algorithms: secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp.");
|
||||
cJSON_AddStringToObject(t68_params, "type", "object");
|
||||
cJSON_AddItemToObject(t68_params, "properties", t68_props);
|
||||
cJSON_AddItemToObject(t68_params, "required", t68_required);
|
||||
|
||||
cJSON* p68_op = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_op, "type", "string");
|
||||
cJSON_AddStringToObject(p68_op, "description",
|
||||
"Operation: get_info | get_public_key | sign | verify | encapsulate | decapsulate | "
|
||||
"derive_shared_secret | derive_hmac | otp_encrypt | otp_decrypt");
|
||||
cJSON_AddItemToObject(t68_props, "operation", p68_op);
|
||||
cJSON* p68_alg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_alg, "type", "string");
|
||||
cJSON_AddStringToObject(p68_alg, "description",
|
||||
"Algorithm: secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp. "
|
||||
"Required for: get_public_key, sign, verify. Not used for: get_info, encapsulate, otp_*.");
|
||||
cJSON_AddItemToObject(t68_props, "algorithm", p68_alg);
|
||||
cJSON* p68_index = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_index, "type", "integer");
|
||||
cJSON_AddStringToObject(p68_index, "description",
|
||||
"Algorithm derivation index. Required for: get_public_key, sign, verify, decapsulate, "
|
||||
"derive_shared_secret. Default: 0.");
|
||||
cJSON_AddItemToObject(t68_props, "index", p68_index);
|
||||
cJSON* p68_scheme = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_scheme, "type", "string");
|
||||
cJSON_AddStringToObject(p68_scheme, "description",
|
||||
"secp256k1-only signing scheme: 'schnorr' (default, BIP-340) or 'ecdsa'. Optional.");
|
||||
cJSON_AddItemToObject(t68_props, "scheme", p68_scheme);
|
||||
cJSON* p68_msg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_msg, "type", "string");
|
||||
cJSON_AddStringToObject(p68_msg, "description",
|
||||
"Message bytes as hex string. Required for: sign, verify.");
|
||||
cJSON_AddItemToObject(t68_props, "message_hex", p68_msg);
|
||||
cJSON* p68_sig = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_sig, "type", "string");
|
||||
cJSON_AddStringToObject(p68_sig, "description",
|
||||
"Signature bytes as hex string. Required for: verify.");
|
||||
cJSON_AddItemToObject(t68_props, "signature_hex", p68_sig);
|
||||
cJSON* p68_peer = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_peer, "type", "string");
|
||||
cJSON_AddStringToObject(p68_peer, "description",
|
||||
"Peer public key as hex string. Required for: encapsulate (ML-KEM-768 pubkey, 2368 hex), "
|
||||
"derive_shared_secret (X25519 pubkey, 64 hex).");
|
||||
cJSON_AddItemToObject(t68_props, "peer_pubkey_hex", p68_peer);
|
||||
cJSON* p68_ct = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_ct, "type", "string");
|
||||
cJSON_AddStringToObject(p68_ct, "description",
|
||||
"Ciphertext as hex string. Required for: decapsulate.");
|
||||
cJSON_AddItemToObject(t68_props, "ciphertext_hex", p68_ct);
|
||||
cJSON* p68_data = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_data, "type", "string");
|
||||
cJSON_AddStringToObject(p68_data, "description",
|
||||
"Arbitrary UTF-8 data string. Required for: derive_hmac.");
|
||||
cJSON_AddItemToObject(t68_props, "data", p68_data);
|
||||
cJSON* p68_pt = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_pt, "type", "string");
|
||||
cJSON_AddStringToObject(p68_pt, "description",
|
||||
"Base64-encoded plaintext. Required for: otp_encrypt.");
|
||||
cJSON_AddItemToObject(t68_props, "plaintext_b64", p68_pt);
|
||||
cJSON* p68_enc = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_enc, "type", "string");
|
||||
cJSON_AddStringToObject(p68_enc, "description",
|
||||
"OTP encoding: 'ascii' (default) or 'binary'. Optional for: otp_encrypt, otp_decrypt.");
|
||||
cJSON_AddItemToObject(t68_props, "encoding", p68_enc);
|
||||
cJSON* p68_ct2 = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p68_ct2, "type", "string");
|
||||
cJSON_AddStringToObject(p68_ct2, "description",
|
||||
"OTP ciphertext (ASCII-armored or base64 blob). Required for: otp_decrypt.");
|
||||
cJSON_AddItemToObject(t68_props, "ciphertext", p68_ct2);
|
||||
|
||||
cJSON_AddItemToArray(t68_required, cJSON_CreateString("operation"));
|
||||
cJSON_AddItemToObject(t68_fn, "parameters", t68_params);
|
||||
cJSON_AddItemToObject(t68, "function", t68_fn);
|
||||
cJSON_AddItemToArray(tools, t68);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user