3113 lines
117 KiB
C
3113 lines
117 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "setup_wizard.h"
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <termios.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/stat.h>
|
|
#include <limits.h>
|
|
#include <pwd.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "llm.h"
|
|
#include "main.h"
|
|
#include "nostr_handler.h"
|
|
#include "default_events.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
#include "../../nostr_core_lib/nostr_core/nsigner_transport.h"
|
|
#endif
|
|
|
|
#define ANSI_UNDERLINE "\x1b[4m"
|
|
#define ANSI_RESET "\x1b[0m"
|
|
#define ANSI_YELLOW "\x1b[33m"
|
|
#define ANSI_RED "\x1b[31m"
|
|
|
|
#define WIZARD_LINE_MAX 1024
|
|
|
|
typedef struct {
|
|
char key;
|
|
const char* label;
|
|
} wizard_option_t;
|
|
|
|
static int is_hex64(const char* s) {
|
|
if (!s || strlen(s) != 64U) return 0;
|
|
for (size_t i = 0; i < 64U; i++) {
|
|
if (!isxdigit((unsigned char)s[i])) return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int decode_pubkey_hex_or_npub_local(const char* input, char out_hex[65]) {
|
|
if (!input || !out_hex) return -1;
|
|
|
|
if (strncmp(input, "npub1", 5) == 0) {
|
|
unsigned char pubkey[32];
|
|
if (nostr_decode_npub(input, pubkey) != 0) {
|
|
return -1;
|
|
}
|
|
nostr_bytes_to_hex(pubkey, 32, out_hex);
|
|
return 0;
|
|
}
|
|
|
|
if (is_hex64(input)) {
|
|
snprintf(out_hex, 65, "%s", input);
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int derive_keys_from_nsec_local(const char* nsec_or_hex, didactyl_config_t* cfg) {
|
|
if (!nsec_or_hex || !cfg) return -1;
|
|
|
|
size_t n = strlen(nsec_or_hex);
|
|
if (n == 0 || n >= sizeof(cfg->keys.nsec)) {
|
|
return -1;
|
|
}
|
|
|
|
snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec_or_hex);
|
|
|
|
int dec_rc = -1;
|
|
if (strncmp(nsec_or_hex, "nsec1", 5) == 0) {
|
|
dec_rc = (nostr_decode_nsec(nsec_or_hex, cfg->keys.private_key) == 0) ? 0 : -1;
|
|
} else if (strlen(nsec_or_hex) == 64U) {
|
|
dec_rc = (nostr_hex_to_bytes(nsec_or_hex, cfg->keys.private_key, 32) == 0) ? 0 : -1;
|
|
}
|
|
if (dec_rc != 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (nostr_ec_public_key_from_private_key(cfg->keys.private_key, cfg->keys.public_key) != 0) {
|
|
return -1;
|
|
}
|
|
nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex);
|
|
return 0;
|
|
}
|
|
|
|
static int line_is_quit(const char* s) {
|
|
if (!s) return 0;
|
|
while (*s && isspace((unsigned char)*s)) s++;
|
|
char c = (char)tolower((unsigned char)*s);
|
|
return c == 'q' || c == 'x';
|
|
}
|
|
|
|
static void trim_line(char* s) {
|
|
if (!s) return;
|
|
size_t n = strlen(s);
|
|
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' || isspace((unsigned char)s[n - 1]))) {
|
|
s[n - 1] = '\0';
|
|
n--;
|
|
}
|
|
char* p = s;
|
|
while (*p && isspace((unsigned char)*p)) p++;
|
|
if (p != s) {
|
|
memmove(s, p, strlen(p) + 1U);
|
|
}
|
|
}
|
|
|
|
static int read_line_prompt(const char* prompt, char* out, size_t out_size) {
|
|
if (!out || out_size == 0) return -1;
|
|
out[0] = '\0';
|
|
if (prompt && prompt[0] != '\0') {
|
|
fprintf(stderr, "%s", prompt);
|
|
fflush(stderr);
|
|
}
|
|
if (!fgets(out, (int)out_size, stdin)) {
|
|
return -1;
|
|
}
|
|
trim_line(out);
|
|
return 0;
|
|
}
|
|
|
|
static int read_secret_prompt(const char* prompt, char* out, size_t out_size) {
|
|
if (!out || out_size == 0) return -1;
|
|
out[0] = '\0';
|
|
|
|
struct termios oldt;
|
|
struct termios newt;
|
|
int disable_echo = 0;
|
|
|
|
if (tcgetattr(STDIN_FILENO, &oldt) == 0) {
|
|
newt = oldt;
|
|
newt.c_lflag &= (tcflag_t)~ECHO;
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) == 0) {
|
|
disable_echo = 1;
|
|
}
|
|
}
|
|
|
|
if (prompt && prompt[0] != '\0') {
|
|
fprintf(stderr, "%s", prompt);
|
|
fflush(stderr);
|
|
}
|
|
|
|
if (!fgets(out, (int)out_size, stdin)) {
|
|
if (disable_echo) {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
|
fprintf(stderr, "\n");
|
|
fflush(stderr);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (disable_echo) {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
|
fprintf(stderr, "\n");
|
|
fflush(stderr);
|
|
}
|
|
|
|
trim_line(out);
|
|
return 0;
|
|
}
|
|
|
|
static char read_menu_choice(const wizard_option_t* options, int count) {
|
|
char line[WIZARD_LINE_MAX];
|
|
if (read_line_prompt("> ", line, sizeof(line)) != 0) {
|
|
return 'q';
|
|
}
|
|
if (line_is_quit(line)) {
|
|
return 'q';
|
|
}
|
|
char c = '\0';
|
|
for (size_t i = 0; line[i] != '\0'; i++) {
|
|
if (!isspace((unsigned char)line[i])) {
|
|
c = (char)tolower((unsigned char)line[i]);
|
|
break;
|
|
}
|
|
}
|
|
if (c == '\0') {
|
|
return '\0';
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
if ((char)tolower((unsigned char)options[i].key) == c) {
|
|
return c;
|
|
}
|
|
}
|
|
return '\0';
|
|
}
|
|
|
|
static void print_option(char key, const char* tail) {
|
|
fprintf(stderr, " %s%c%s%s\n", ANSI_UNDERLINE, (int)toupper((unsigned char)key), ANSI_RESET, tail ? tail : "");
|
|
}
|
|
|
|
static int get_terminal_width(void) {
|
|
struct winsize w;
|
|
if (ioctl(STDERR_FILENO, TIOCGWINSZ, &w) == 0) {
|
|
return w.ws_col;
|
|
}
|
|
return 80; // Fallback
|
|
}
|
|
|
|
static void print_centered(const char* s, int width) {
|
|
int len = (int)strlen(s);
|
|
if (len >= width) {
|
|
fprintf(stderr, "%s\n", s);
|
|
return;
|
|
}
|
|
int padding = (width - len) / 2;
|
|
for (int i = 0; i < padding; i++) fputc(' ', stderr);
|
|
fprintf(stderr, "%s\n", s);
|
|
}
|
|
|
|
static void print_full_width_line(char c, int width) {
|
|
for (int i = 0; i < width; i++) fputc(c, stderr);
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
static void print_interactive_splash(void) {
|
|
int width = get_terminal_width();
|
|
fprintf(stderr, "\033[H\033[J\n\n\n\n");
|
|
|
|
const char* art[] = {
|
|
"$$$$$$$\\ $$\\ $$\\ $$\\ $$\\ ",
|
|
"$$ __$$\\ \\__| $$ | $$ | $$ |",
|
|
"$$ | $$ |$$\\ $$$$$$$ | $$$$$$\\ $$$$$$$\\ $$$$$$\\ $$\\ $$\\ $$ |",
|
|
"$$ | $$ |$$ |$$ __$$ | \\____$$\\ $$ _____|\\_$$ _| $$ | $$ |$$ |",
|
|
"$$ | $$ |$$ |$$ / $$ | $$$$$$$ |$$ / $$ | $$ | $$ |$$ |",
|
|
"$$ | $$ |$$ |$$ | $$ |$$ __$$ |$$ | $$ |$$\\ $$ | $$ |$$ |",
|
|
"$$$$$$$ |$$ |\\$$$$$$$ |\\$$$$$$$ |\\$$$$$$$\\ \\$$$$ |\\$$$$$$$ |$$ |",
|
|
"\\_______/ \\__| \\_______| \\_______| \\_______| \\____/ \\____$$ |\\__|",
|
|
" $$\\ $$ | ",
|
|
" \\$$$$$$ | ",
|
|
" \\______/ "
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(art) / sizeof(art[0]); i++) {
|
|
print_centered(art[i], width);
|
|
}
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
static void render_wizard_page_header(const char* step_title, const char* section_title) {
|
|
int width = get_terminal_width();
|
|
char title[256];
|
|
snprintf(title, sizeof(title), "Didactyl -- %s -- Interactive Setup", DIDACTYL_VERSION);
|
|
|
|
print_interactive_splash();
|
|
print_full_width_line('=', width);
|
|
print_full_width_line('=', width);
|
|
print_centered(title, width);
|
|
print_centered("Not your keys, not your agent", width);
|
|
if (section_title && section_title[0] != '\0') {
|
|
print_centered(section_title, width);
|
|
}
|
|
if (step_title && step_title[0] != '\0') {
|
|
print_centered(step_title, width);
|
|
}
|
|
print_full_width_line('=', width);
|
|
print_full_width_line('=', width);
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
static void config_set_defaults(didactyl_config_t* cfg) {
|
|
if (!cfg) return;
|
|
memset(cfg, 0, sizeof(*cfg));
|
|
|
|
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
|
|
|
cfg->tools.enabled = 1;
|
|
cfg->tools.max_turns = 40;
|
|
cfg->tools.trigger_max_turns = 12;
|
|
cfg->tools.api_default_max_turns = 8;
|
|
cfg->tools.api_max_turns_ceiling = 32;
|
|
cfg->tools.stall_repeat_threshold = 3;
|
|
cfg->tools.max_context_bytes = 128 * 1024;
|
|
cfg->tools.local_http_fetch_default_timeout_seconds = 20;
|
|
cfg->tools.local_http_fetch_max_timeout_seconds = 120;
|
|
cfg->tools.blossom_max_upload_bytes = 16 * 1024 * 1024;
|
|
cfg->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
|
|
cfg->tools.shell.enabled = 1;
|
|
cfg->tools.shell.timeout_seconds = 30;
|
|
cfg->tools.shell.max_output_bytes = 65536;
|
|
snprintf(cfg->tools.shell.working_directory, sizeof(cfg->tools.shell.working_directory), "%s", ".");
|
|
|
|
cfg->security.verify_signatures = 1;
|
|
cfg->security.admin.enabled = 1;
|
|
cfg->security.admin.tools_enabled = 1;
|
|
cfg->security.wot.enabled = 1;
|
|
cfg->security.wot.tools_enabled = 0;
|
|
cfg->security.stranger.enabled = 1;
|
|
cfg->security.stranger.tools_enabled = 0;
|
|
|
|
cfg->admin_context.enabled = 1;
|
|
cfg->admin_context.track_kind_0 = 1;
|
|
cfg->admin_context.track_kind_3 = 1;
|
|
cfg->admin_context.track_kind_10002 = 1;
|
|
cfg->admin_context.track_kind_1 = 1;
|
|
cfg->admin_context.kind_1_limit = 10;
|
|
|
|
cfg->triggers.enabled = 1;
|
|
cfg->triggers.max_active = 16;
|
|
cfg->triggers.cooldown_seconds = 60;
|
|
cfg->triggers.llm_rate_limit_per_minute = 10;
|
|
cfg->triggers.template_rate_limit_per_minute = 60;
|
|
|
|
cfg->api.enabled = 1;
|
|
cfg->api.port = 8484;
|
|
snprintf(cfg->api.bind_address, sizeof(cfg->api.bind_address), "%s", "127.0.0.1");
|
|
|
|
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai");
|
|
snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", "gpt-4o-mini");
|
|
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", "https://api.openai.com/v1");
|
|
cfg->llm.max_tokens = 512;
|
|
cfg->llm.temperature = 0.7;
|
|
|
|
/* Signer provider defaults: local mode, backward compatible.
|
|
* Without these, apply_signer_overrides() in main.c rejects an empty
|
|
* 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.timeout_ms = 15000;
|
|
}
|
|
|
|
static void free_relays_only(didactyl_config_t* cfg) {
|
|
if (!cfg || !cfg->relays) return;
|
|
for (int i = 0; i < cfg->relay_count; i++) {
|
|
free(cfg->relays[i]);
|
|
}
|
|
free(cfg->relays);
|
|
cfg->relays = NULL;
|
|
cfg->relay_count = 0;
|
|
}
|
|
|
|
static int set_default_relays(didactyl_config_t* cfg) {
|
|
if (!cfg) return -1;
|
|
free_relays_only(cfg);
|
|
cfg->relay_count = DIDACTYL_DEFAULT_RELAY_COUNT;
|
|
cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*));
|
|
if (!cfg->relays) {
|
|
cfg->relay_count = 0;
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < cfg->relay_count; i++) {
|
|
cfg->relays[i] = strdup(DIDACTYL_DEFAULT_RELAYS[i]);
|
|
if (!cfg->relays[i]) {
|
|
free_relays_only(cfg);
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int build_relay_tags_json_with_key(const didactyl_config_t* cfg,
|
|
const char* tag_key,
|
|
char** out_tags_json) {
|
|
if (!cfg || !tag_key || tag_key[0] == '\0' || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) {
|
|
return -1;
|
|
}
|
|
*out_tags_json = NULL;
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) return -1;
|
|
|
|
for (int i = 0; i < cfg->relay_count; i++) {
|
|
if (!cfg->relays[i] || cfg->relays[i][0] == '\0') continue;
|
|
cJSON* tag = cJSON_CreateArray();
|
|
if (!tag) {
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(tag_key));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(cfg->relays[i]));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
char* json = cJSON_PrintUnformatted(tags);
|
|
cJSON_Delete(tags);
|
|
if (!json) return -1;
|
|
*out_tags_json = json;
|
|
return 0;
|
|
}
|
|
|
|
static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
|
|
return build_relay_tags_json_with_key(cfg, "r", out_tags_json);
|
|
}
|
|
|
|
static int build_kind10050_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
|
|
return build_relay_tags_json_with_key(cfg, "relay", out_tags_json);
|
|
}
|
|
|
|
static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) {
|
|
if (!tags_json || !out || out_size == 0) {
|
|
return -1;
|
|
}
|
|
|
|
out[0] = '\0';
|
|
cJSON* tags = cJSON_Parse(tags_json);
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
|
|
if (strcmp(k->valuestring, "d") == 0 && v->valuestring[0] != '\0') {
|
|
snprintf(out, out_size, "%s", v->valuestring);
|
|
cJSON_Delete(tags);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(tags);
|
|
return -1;
|
|
}
|
|
|
|
static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
|
|
if (!cfg || !content || !tags_json) {
|
|
return -1;
|
|
}
|
|
|
|
int is_skill_kind = (kind == 31123 || kind == 31124);
|
|
char incoming_d[128] = {0};
|
|
int incoming_has_d = (is_skill_kind && tags_json_get_d_tag_local(tags_json, incoming_d, sizeof(incoming_d)) == 0);
|
|
|
|
for (int i = 0; i < cfg->startup_event_count; i++) {
|
|
startup_event_t* se = &cfg->startup_events[i];
|
|
if (se->kind != kind) {
|
|
continue;
|
|
}
|
|
|
|
if (is_skill_kind) {
|
|
if (!incoming_has_d) {
|
|
continue;
|
|
}
|
|
char existing_d[128] = {0};
|
|
if (tags_json_get_d_tag_local(se->tags_json, existing_d, sizeof(existing_d)) != 0) {
|
|
continue;
|
|
}
|
|
if (strcmp(existing_d, incoming_d) != 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
char* content_dup = strdup(content);
|
|
char* tags_dup = strdup(tags_json);
|
|
if (!content_dup || !tags_dup) {
|
|
free(content_dup);
|
|
free(tags_dup);
|
|
return -1;
|
|
}
|
|
|
|
free(se->content);
|
|
free(se->tags_json);
|
|
se->content = content_dup;
|
|
se->tags_json = tags_dup;
|
|
return 0;
|
|
}
|
|
|
|
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
|
|
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
|
|
if (!grown) {
|
|
return -1;
|
|
}
|
|
cfg->startup_events = grown;
|
|
|
|
startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
|
|
memset(se, 0, sizeof(*se));
|
|
se->kind = kind;
|
|
se->content = strdup(content);
|
|
se->tags_json = strdup(tags_json);
|
|
if (!se->content || !se->tags_json) {
|
|
free(se->content);
|
|
free(se->tags_json);
|
|
se->content = NULL;
|
|
se->tags_json = NULL;
|
|
return -1;
|
|
}
|
|
|
|
cfg->startup_event_count++;
|
|
return 0;
|
|
}
|
|
|
|
static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) {
|
|
if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) {
|
|
int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i];
|
|
|
|
if (kind == DIDACTYL_STARTUP_KIND_PROFILE) {
|
|
int needed = snprintf(NULL,
|
|
0,
|
|
DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
|
|
agent_name,
|
|
agent_name);
|
|
if (needed <= 0) {
|
|
return -1;
|
|
}
|
|
char* content_json = (char*)malloc((size_t)needed + 1U);
|
|
if (!content_json) {
|
|
return -1;
|
|
}
|
|
snprintf(content_json,
|
|
(size_t)needed + 1U,
|
|
DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
|
|
agent_name,
|
|
agent_name);
|
|
|
|
int rc = upsert_startup_event(cfg, kind, content_json, "[]");
|
|
free(content_json);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (kind == DIDACTYL_STARTUP_KIND_CONTACTS) {
|
|
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
|
|
if (needed <= 0) {
|
|
return -1;
|
|
}
|
|
char* tags_json = (char*)malloc((size_t)needed + 1U);
|
|
if (!tags_json) {
|
|
return -1;
|
|
}
|
|
snprintf(tags_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
|
|
|
|
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND3_CONTENT, tags_json);
|
|
free(tags_json);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (kind == DIDACTYL_STARTUP_KIND_RELAYS) {
|
|
char* tags_json = NULL;
|
|
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
|
free(tags_json);
|
|
return -1;
|
|
}
|
|
|
|
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json);
|
|
free(tags_json);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (kind == DIDACTYL_STARTUP_KIND_DM_RELAYS) {
|
|
char* tags_json = NULL;
|
|
if (build_kind10050_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
|
free(tags_json);
|
|
return -1;
|
|
}
|
|
|
|
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10050_CONTENT, tags_json);
|
|
free(tags_json);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int configure_default_skills_for_agent(didactyl_config_t* cfg, const char* agent_name) {
|
|
if (!cfg || !agent_name || agent_name[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
if (upsert_startup_event(cfg,
|
|
DIDACTYL_DEFAULT_SKILL_KIND,
|
|
DIDACTYL_DEFAULT_SKILL_TEMPLATE,
|
|
DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (upsert_startup_event(cfg,
|
|
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND,
|
|
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE,
|
|
DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int relay_contains(const didactyl_config_t* cfg, const char* relay) {
|
|
if (!cfg || !relay || relay[0] == '\0') return 0;
|
|
for (int i = 0; i < cfg->relay_count; i++) {
|
|
if (cfg->relays[i] && strcmp(cfg->relays[i], relay) == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int relay_add(didactyl_config_t* cfg, const char* relay) {
|
|
if (!cfg || !relay || relay[0] == '\0') return -1;
|
|
if (relay_contains(cfg, relay)) return 0;
|
|
|
|
char** grown = (char**)realloc(cfg->relays, (size_t)(cfg->relay_count + 1) * sizeof(char*));
|
|
if (!grown) return -1;
|
|
cfg->relays = grown;
|
|
cfg->relays[cfg->relay_count] = strdup(relay);
|
|
if (!cfg->relays[cfg->relay_count]) return -1;
|
|
cfg->relay_count++;
|
|
return 0;
|
|
}
|
|
|
|
static int relay_remove_index(didactyl_config_t* cfg, int idx) {
|
|
if (!cfg || idx < 0 || idx >= cfg->relay_count || cfg->relay_count <= 1) return -1;
|
|
free(cfg->relays[idx]);
|
|
for (int i = idx; i < cfg->relay_count - 1; i++) {
|
|
cfg->relays[i] = cfg->relays[i + 1];
|
|
}
|
|
cfg->relay_count--;
|
|
if (cfg->relay_count == 0) {
|
|
free(cfg->relays);
|
|
cfg->relays = NULL;
|
|
return 0;
|
|
}
|
|
char** shrunk = (char**)realloc(cfg->relays, (size_t)cfg->relay_count * sizeof(char*));
|
|
if (shrunk) cfg->relays = shrunk;
|
|
return 0;
|
|
}
|
|
|
|
static int wait_connected_relays_ms(int timeout_ms) {
|
|
const int step = 100;
|
|
int elapsed = 0;
|
|
while (elapsed < timeout_ms) {
|
|
if (nostr_handler_connected_relay_count() > 0) return 1;
|
|
(void)nostr_handler_poll(step);
|
|
elapsed += step;
|
|
}
|
|
return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
|
|
}
|
|
|
|
static int query_self_kind10002_exists(const didactyl_config_t* cfg, int* out_exists) {
|
|
if (!cfg || !out_exists || cfg->keys.public_key_hex[0] == '\0') return -1;
|
|
*out_exists = 0;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 3000);
|
|
cJSON_Delete(filter);
|
|
|
|
if (events_json) {
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
if (arr && cJSON_IsArray(arr) && cJSON_GetArraySize(arr) > 0) {
|
|
*out_exists = 1;
|
|
}
|
|
cJSON_Delete(arr);
|
|
free(events_json);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int query_admin_kind0_name(const didactyl_config_t* cfg,
|
|
char* out_name,
|
|
size_t out_name_size,
|
|
int* out_found) {
|
|
if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->admin.pubkey[0] == '\0') {
|
|
return -1;
|
|
}
|
|
out_name[0] = '\0';
|
|
*out_found = 0;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->admin.pubkey));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 3000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
|
if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* profile = cJSON_Parse(content->valuestring);
|
|
if (!profile || !cJSON_IsObject(profile)) {
|
|
cJSON_Delete(profile);
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name");
|
|
cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name");
|
|
cJSON* chosen = NULL;
|
|
|
|
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
|
|
chosen = display_name;
|
|
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
|
chosen = name;
|
|
}
|
|
|
|
if (chosen && chosen->valuestring) {
|
|
snprintf(out_name, out_name_size, "%s", chosen->valuestring);
|
|
*out_found = 1;
|
|
}
|
|
|
|
cJSON_Delete(profile);
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
static int validate_new_agent_identity_and_admin(const didactyl_config_t* cfg,
|
|
int* out_exists,
|
|
char* out_admin_name,
|
|
size_t out_admin_name_size,
|
|
int* out_admin_name_found) {
|
|
if (!cfg || !out_exists || !out_admin_name || out_admin_name_size == 0 || !out_admin_name_found) {
|
|
return -1;
|
|
}
|
|
|
|
*out_exists = 0;
|
|
*out_admin_name_found = 0;
|
|
out_admin_name[0] = '\0';
|
|
|
|
didactyl_config_t tmp = *cfg;
|
|
if (nostr_handler_init(&tmp) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
(void)wait_connected_relays_ms(4000);
|
|
|
|
int rc_exists = query_self_kind10002_exists(cfg, out_exists);
|
|
int rc_admin = query_admin_kind0_name(cfg, out_admin_name, out_admin_name_size, out_admin_name_found);
|
|
|
|
nostr_handler_cleanup();
|
|
return (rc_exists == 0 && rc_admin == 0) ? 0 : -1;
|
|
}
|
|
|
|
static int query_and_extract_kind10002_relays(didactyl_config_t* cfg) {
|
|
if (!cfg || cfg->keys.public_key_hex[0] == '\0') return -1;
|
|
|
|
didactyl_config_t tmp = *cfg;
|
|
if (nostr_handler_init(&tmp) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
(void)wait_connected_relays_ms(5000);
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 8);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 4000);
|
|
cJSON_Delete(filter);
|
|
|
|
if (!events_json) {
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(arr);
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
didactyl_config_t new_relays = {0};
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
|
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
|
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) continue;
|
|
if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
|
|
(void)relay_add(&new_relays, val->valuestring);
|
|
}
|
|
}
|
|
|
|
if (new_relays.relay_count > 0) {
|
|
free_relays_only(cfg);
|
|
cfg->relays = new_relays.relays;
|
|
cfg->relay_count = new_relays.relay_count;
|
|
} else {
|
|
free_relays_only(&new_relays);
|
|
}
|
|
|
|
cJSON_Delete(arr);
|
|
nostr_handler_cleanup();
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_relay_found) {
|
|
if (!cfg) return -1;
|
|
if (out_relay_found) *out_relay_found = 0;
|
|
|
|
if (query_and_extract_kind10002_relays(cfg) == 0) {
|
|
if (out_relay_found) *out_relay_found = cfg->relay_count > 0 ? 1 : 0;
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int query_self_kind0_name(const didactyl_config_t* cfg,
|
|
char* out_name,
|
|
size_t out_name_size,
|
|
int* out_found) {
|
|
if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->keys.public_key_hex[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
out_name[0] = '\0';
|
|
*out_found = 0;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 3000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
|
if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* profile = cJSON_Parse(content->valuestring);
|
|
if (!profile || !cJSON_IsObject(profile)) {
|
|
cJSON_Delete(profile);
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name");
|
|
cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name");
|
|
cJSON* chosen = NULL;
|
|
|
|
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
|
|
chosen = display_name;
|
|
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
|
chosen = name;
|
|
}
|
|
|
|
if (chosen && chosen->valuestring) {
|
|
snprintf(out_name, out_name_size, "%s", chosen->valuestring);
|
|
*out_found = 1;
|
|
}
|
|
|
|
cJSON_Delete(profile);
|
|
cJSON_Delete(arr);
|
|
return 0;
|
|
}
|
|
|
|
static int fetch_and_decrypt_self_config_wizard(const didactyl_config_t* cfg,
|
|
const char* d_tag,
|
|
nostr_signer_t* signer,
|
|
char** out_plaintext) {
|
|
if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
*out_plaintext = NULL;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
cJSON* d_vals = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors || !d_vals) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
cJSON_Delete(d_vals);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag));
|
|
cJSON_AddItemToObject(filter, "#d", d_vals);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 4000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
|
if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
size_t out_cap = strlen(content->valuestring) + 1024U;
|
|
char* plaintext = (char*)malloc(out_cap);
|
|
if (!plaintext) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
/* Route NIP-44 decrypt through the signer when one is available so
|
|
* remote signer modes keep the nsec out of the agent process. The peer
|
|
* pubkey for self-encrypt is the agent's own pubkey. Falls back to the
|
|
* raw-key path when signer == NULL (local mode / legacy). */
|
|
int dec_rc = NOSTR_ERROR_CRYPTO_FAILED;
|
|
if (signer) {
|
|
char* signer_plaintext = NULL;
|
|
dec_rc = nostr_signer_nip44_decrypt(signer,
|
|
cfg->keys.public_key_hex,
|
|
content->valuestring,
|
|
&signer_plaintext);
|
|
cJSON_Delete(arr);
|
|
if (dec_rc == NOSTR_SUCCESS && signer_plaintext) {
|
|
free(plaintext);
|
|
*out_plaintext = signer_plaintext;
|
|
return 0;
|
|
}
|
|
free(signer_plaintext);
|
|
free(plaintext);
|
|
return -1;
|
|
}
|
|
|
|
dec_rc = nostr_nip44_decrypt(cfg->keys.private_key,
|
|
cfg->keys.public_key,
|
|
content->valuestring,
|
|
plaintext,
|
|
out_cap);
|
|
cJSON_Delete(arr);
|
|
if (dec_rc != NOSTR_SUCCESS) {
|
|
free(plaintext);
|
|
return -1;
|
|
}
|
|
|
|
*out_plaintext = plaintext;
|
|
return 0;
|
|
}
|
|
|
|
static int apply_recalled_user_settings_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
|
if (!cfg || !plaintext) return -1;
|
|
|
|
cJSON* root = cJSON_Parse(plaintext);
|
|
if (!root || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
|
cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl");
|
|
if (!global_llm || !cJSON_IsObject(global_llm)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* provider = cJSON_GetObjectItemCaseSensitive(global_llm, "provider");
|
|
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(global_llm, "api_key");
|
|
cJSON* model = cJSON_GetObjectItemCaseSensitive(global_llm, "model");
|
|
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(global_llm, "base_url");
|
|
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(global_llm, "max_tokens");
|
|
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(global_llm, "temperature");
|
|
|
|
cJSON* admin_pubkey = NULL;
|
|
cJSON* dm_protocol = NULL;
|
|
cJSON* max_turns = NULL;
|
|
if (didactyl && cJSON_IsObject(didactyl)) {
|
|
admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey");
|
|
dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol");
|
|
max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns");
|
|
}
|
|
|
|
if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' ||
|
|
!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' ||
|
|
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') {
|
|
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring);
|
|
} else if (cfg->llm.provider[0] == '\0') {
|
|
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai");
|
|
}
|
|
|
|
snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring);
|
|
snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring);
|
|
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring);
|
|
if (max_tokens && cJSON_IsNumber(max_tokens)) {
|
|
cfg->llm.max_tokens = (int)max_tokens->valuedouble;
|
|
}
|
|
if (temperature && cJSON_IsNumber(temperature)) {
|
|
cfg->llm.temperature = temperature->valuedouble;
|
|
}
|
|
|
|
if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
|
|
char decoded[65] = {0};
|
|
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) != 0) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
|
}
|
|
|
|
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
|
|
if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
|
|
cfg->dm_protocol = DM_PROTOCOL_NIP17;
|
|
} else if (strcmp(dm_protocol->valuestring, "both") == 0) {
|
|
cfg->dm_protocol = DM_PROTOCOL_BOTH;
|
|
} else {
|
|
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
|
}
|
|
}
|
|
|
|
if (max_turns && cJSON_IsNumber(max_turns)) {
|
|
int parsed = (int)max_turns->valuedouble;
|
|
cfg->tools.max_turns = (parsed > 0) ? parsed : 40;
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
static int recover_full_config_from_nostr(didactyl_config_t* cfg,
|
|
nostr_signer_t* signer,
|
|
char* out_agent_name,
|
|
size_t out_agent_name_size,
|
|
int* out_agent_name_found) {
|
|
if (!cfg || !out_agent_name || out_agent_name_size == 0 || !out_agent_name_found) {
|
|
return -1;
|
|
}
|
|
|
|
out_agent_name[0] = '\0';
|
|
*out_agent_name_found = 0;
|
|
|
|
didactyl_config_t tmp = *cfg;
|
|
if (nostr_handler_init(&tmp) != 0) {
|
|
return -1;
|
|
}
|
|
if (signer) {
|
|
nostr_handler_set_signer(signer);
|
|
}
|
|
|
|
(void)wait_connected_relays_ms(5000);
|
|
|
|
(void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found);
|
|
|
|
char* settings_plaintext = NULL;
|
|
if (fetch_and_decrypt_self_config_wizard(cfg, "user-settings", signer, &settings_plaintext) == 0 && settings_plaintext) {
|
|
(void)apply_recalled_user_settings_wizard(cfg, settings_plaintext);
|
|
}
|
|
free(settings_plaintext);
|
|
|
|
nostr_handler_cleanup();
|
|
return 0;
|
|
}
|
|
|
|
static const char* dm_protocol_to_string_local(dm_protocol_t protocol) {
|
|
switch (protocol) {
|
|
case DM_PROTOCOL_NIP17: return "nip17";
|
|
case DM_PROTOCOL_BOTH: return "both";
|
|
case DM_PROTOCOL_NIP04:
|
|
default:
|
|
return "nip04";
|
|
}
|
|
}
|
|
|
|
static int publish_encrypted_self_config_wizard(const didactyl_config_t* cfg,
|
|
const char* d_tag,
|
|
nostr_signer_t* signer,
|
|
const char* content_json) {
|
|
if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1;
|
|
|
|
size_t cipher_cap = (strlen(content_json) * 4U) + 1024U;
|
|
char* ciphertext = (char*)malloc(cipher_cap);
|
|
if (!ciphertext) return -1;
|
|
|
|
/* Route NIP-44 encrypt through the signer when one is available so
|
|
* remote signer modes keep the nsec out of the agent process. Falls back
|
|
* to the raw-key path when signer == NULL (local mode / legacy). */
|
|
int enc_rc = NOSTR_ERROR_CRYPTO_FAILED;
|
|
if (signer) {
|
|
char* signer_cipher = NULL;
|
|
enc_rc = nostr_signer_nip44_encrypt(signer,
|
|
cfg->keys.public_key_hex,
|
|
content_json,
|
|
&signer_cipher);
|
|
if (enc_rc == NOSTR_SUCCESS && signer_cipher) {
|
|
free(ciphertext);
|
|
ciphertext = signer_cipher;
|
|
} else {
|
|
free(signer_cipher);
|
|
free(ciphertext);
|
|
return -1;
|
|
}
|
|
} else {
|
|
enc_rc = nostr_nip44_encrypt(cfg->keys.private_key,
|
|
cfg->keys.public_key,
|
|
content_json,
|
|
ciphertext,
|
|
cipher_cap);
|
|
if (enc_rc != NOSTR_SUCCESS) {
|
|
free(ciphertext);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
cJSON* d = cJSON_CreateArray();
|
|
cJSON* app = cJSON_CreateArray();
|
|
if (!tags || !d || !app) {
|
|
cJSON_Delete(tags);
|
|
cJSON_Delete(d);
|
|
cJSON_Delete(app);
|
|
free(ciphertext);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(d, cJSON_CreateString("d"));
|
|
cJSON_AddItemToArray(d, cJSON_CreateString(d_tag));
|
|
cJSON_AddItemToArray(tags, d);
|
|
|
|
cJSON_AddItemToArray(app, cJSON_CreateString("app"));
|
|
cJSON_AddItemToArray(app, cJSON_CreateString("didactyl"));
|
|
cJSON_AddItemToArray(tags, app);
|
|
|
|
nostr_publish_result_t result;
|
|
memset(&result, 0, sizeof(result));
|
|
int rc = nostr_handler_publish_kind_event(30078, ciphertext, tags, &result);
|
|
nostr_handler_publish_result_free(&result);
|
|
cJSON_Delete(tags);
|
|
free(ciphertext);
|
|
return rc;
|
|
}
|
|
|
|
static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg,
|
|
nostr_signer_t* signer) {
|
|
if (!cfg) return -1;
|
|
|
|
cJSON* user_settings = cJSON_CreateObject();
|
|
cJSON* global_llm = cJSON_CreateObject();
|
|
cJSON* didactyl = cJSON_CreateObject();
|
|
if (!user_settings || !global_llm || !didactyl) {
|
|
cJSON_Delete(user_settings);
|
|
cJSON_Delete(global_llm);
|
|
cJSON_Delete(didactyl);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddNumberToObject(user_settings, "v", 2);
|
|
cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL));
|
|
|
|
cJSON_AddStringToObject(global_llm, "provider", cfg->llm.provider);
|
|
cJSON_AddStringToObject(global_llm, "api_key", cfg->llm.api_key);
|
|
cJSON_AddStringToObject(global_llm, "model", cfg->llm.model);
|
|
cJSON_AddStringToObject(global_llm, "base_url", cfg->llm.base_url);
|
|
cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->llm.max_tokens);
|
|
cJSON_AddNumberToObject(global_llm, "temperature", cfg->llm.temperature);
|
|
cJSON_AddItemToObject(user_settings, "global_llm", global_llm);
|
|
|
|
cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey);
|
|
cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol));
|
|
cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40);
|
|
cJSON_AddItemToObject(user_settings, "didactyl", didactyl);
|
|
|
|
cJSON* signer_obj = cJSON_CreateObject();
|
|
if (signer_obj) {
|
|
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);
|
|
cJSON_AddNumberToObject(signer_obj, "timeout_ms", cfg->signer.timeout_ms);
|
|
cJSON_AddItemToObject(user_settings, "signer", signer_obj);
|
|
}
|
|
|
|
char* settings_json = cJSON_PrintUnformatted(user_settings);
|
|
cJSON_Delete(user_settings);
|
|
if (!settings_json) return -1;
|
|
|
|
int rc = publish_encrypted_self_config_wizard(cfg, "user-settings", signer, settings_json);
|
|
free(settings_json);
|
|
return rc;
|
|
}
|
|
|
|
static int persist_runtime_config_to_nostr_wizard_online(const didactyl_config_t* cfg,
|
|
nostr_signer_t* signer) {
|
|
if (!cfg) return -1;
|
|
|
|
didactyl_config_t tmp = *cfg;
|
|
if (nostr_handler_init(&tmp) != 0) {
|
|
return -1;
|
|
}
|
|
if (signer) {
|
|
nostr_handler_set_signer(signer);
|
|
}
|
|
|
|
(void)wait_connected_relays_ms(5000);
|
|
int rc = persist_runtime_config_to_nostr_wizard(cfg, signer);
|
|
|
|
/* Give relays time to acknowledge the published events before tearing
|
|
down the connection. Without this, the fire-and-forget publish may
|
|
not reach any relay before cleanup closes the sockets. */
|
|
for (int i = 0; i < 30; i++) {
|
|
nostr_handler_poll(100);
|
|
}
|
|
|
|
nostr_handler_cleanup();
|
|
return rc;
|
|
}
|
|
|
|
static int publish_kind10002_relays_wizard_online(const didactyl_config_t* cfg) {
|
|
if (!cfg) return -1;
|
|
|
|
didactyl_config_t tmp = *cfg;
|
|
if (nostr_handler_init(&tmp) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
(void)wait_connected_relays_ms(5000);
|
|
|
|
char* tags_json = NULL;
|
|
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
|
free(tags_json);
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
cJSON* tags = cJSON_Parse(tags_json);
|
|
free(tags_json);
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(tags);
|
|
nostr_handler_cleanup();
|
|
return -1;
|
|
}
|
|
|
|
nostr_publish_result_t result;
|
|
memset(&result, 0, sizeof(result));
|
|
int rc = nostr_handler_publish_kind_event(10002, "", tags, &result);
|
|
nostr_handler_publish_result_free(&result);
|
|
cJSON_Delete(tags);
|
|
|
|
nostr_handler_cleanup();
|
|
return rc;
|
|
}
|
|
|
|
static int ensure_startup_kind10002_from_current_relays(didactyl_config_t* cfg) {
|
|
if (!cfg) return -1;
|
|
|
|
char* tags_json = NULL;
|
|
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
|
free(tags_json);
|
|
return -1;
|
|
}
|
|
|
|
int rc = upsert_startup_event(cfg, 10002, "", tags_json);
|
|
free(tags_json);
|
|
return rc;
|
|
}
|
|
|
|
static int wizard_llm_test(const llm_config_t* llm_cfg) {
|
|
if (!llm_cfg) return -1;
|
|
if (llm_init(llm_cfg) != 0) {
|
|
return -1;
|
|
}
|
|
char* response = llm_chat("You are a setup validator.", "Reply with exactly: OK");
|
|
llm_cleanup();
|
|
if (!response) {
|
|
return -1;
|
|
}
|
|
free(response);
|
|
return 0;
|
|
}
|
|
|
|
static int strcasestr_like(const char* haystack, const char* needle) {
|
|
if (!haystack || !needle) return 0;
|
|
if (needle[0] == '\0') return 1;
|
|
|
|
size_t hlen = strlen(haystack);
|
|
size_t nlen = strlen(needle);
|
|
if (nlen > hlen) return 0;
|
|
|
|
for (size_t i = 0; i + nlen <= hlen; i++) {
|
|
size_t j = 0;
|
|
while (j < nlen) {
|
|
unsigned char hc = (unsigned char)tolower((unsigned char)haystack[i + j]);
|
|
unsigned char nc = (unsigned char)tolower((unsigned char)needle[j]);
|
|
if (hc != nc) break;
|
|
j++;
|
|
}
|
|
if (j == nlen) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void append_model_id_local(cJSON* ids, cJSON* item) {
|
|
if (!ids || !item) return;
|
|
if (cJSON_IsString(item) && item->valuestring) {
|
|
cJSON_AddItemToArray(ids, cJSON_CreateString(item->valuestring));
|
|
return;
|
|
}
|
|
if (cJSON_IsObject(item)) {
|
|
cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id");
|
|
if (id && cJSON_IsString(id) && id->valuestring) {
|
|
cJSON_AddItemToArray(ids, cJSON_CreateString(id->valuestring));
|
|
}
|
|
}
|
|
}
|
|
|
|
static int fetch_models_for_llm_config(const llm_config_t* llm_cfg, cJSON** out_models) {
|
|
if (!llm_cfg || !out_models) return -1;
|
|
*out_models = NULL;
|
|
|
|
if (llm_init(llm_cfg) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
char* raw = llm_list_models_json(NULL);
|
|
llm_cleanup();
|
|
if (!raw) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* root = cJSON_Parse(raw);
|
|
free(raw);
|
|
if (!root) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* ids = cJSON_CreateArray();
|
|
if (!ids) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
if (cJSON_IsArray(root)) {
|
|
int n = cJSON_GetArraySize(root);
|
|
for (int i = 0; i < n; i++) {
|
|
append_model_id_local(ids, cJSON_GetArrayItem(root, i));
|
|
}
|
|
} else if (cJSON_IsObject(root)) {
|
|
cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data");
|
|
if (data && cJSON_IsArray(data)) {
|
|
int n = cJSON_GetArraySize(data);
|
|
for (int i = 0; i < n; i++) {
|
|
append_model_id_local(ids, cJSON_GetArrayItem(data, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
*out_models = ids;
|
|
return 0;
|
|
}
|
|
|
|
static int extract_balance_value(const cJSON* root, double* out_balance) {
|
|
if (!root || !out_balance || !cJSON_IsObject(root)) return -1;
|
|
|
|
const char* keys[] = {
|
|
"balance", "credits", "credit", "remaining", "remaining_credits", "available_balance"
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
|
cJSON* v = cJSON_GetObjectItemCaseSensitive((cJSON*)root, keys[i]);
|
|
if (v && cJSON_IsNumber(v)) {
|
|
*out_balance = v->valuedouble;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cJSON* data = cJSON_GetObjectItemCaseSensitive((cJSON*)root, "data");
|
|
if (data && cJSON_IsObject(data)) {
|
|
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
|
cJSON* v = cJSON_GetObjectItemCaseSensitive(data, keys[i]);
|
|
if (v && cJSON_IsNumber(v)) {
|
|
*out_balance = v->valuedouble;
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static void wizard_check_provider_balance(const llm_config_t* llm_cfg) {
|
|
if (!llm_cfg || llm_cfg->base_url[0] == '\0') return;
|
|
|
|
const char* paths_ppq[] = {"/credits", "/credit", "/balance", "/v1/credits"};
|
|
const char* paths_routstr[] = {"/balance", "/credits", "/v1/balance"};
|
|
const char** paths = NULL;
|
|
size_t path_count = 0;
|
|
|
|
if (strcmp(llm_cfg->provider, "ppq") == 0) {
|
|
paths = paths_ppq;
|
|
path_count = sizeof(paths_ppq) / sizeof(paths_ppq[0]);
|
|
} else if (strcmp(llm_cfg->provider, "routstr") == 0) {
|
|
paths = paths_routstr;
|
|
path_count = sizeof(paths_routstr) / sizeof(paths_routstr[0]);
|
|
} else {
|
|
fprintf(stderr, " Balance check: skipped for provider '%s'\n", llm_cfg->provider);
|
|
return;
|
|
}
|
|
|
|
if (llm_init(llm_cfg) != 0) {
|
|
fprintf(stderr, " Balance check: unavailable (LLM init failed)\n");
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < path_count; i++) {
|
|
char* raw = llm_get_json_path(NULL, paths[i]);
|
|
if (!raw) continue;
|
|
|
|
cJSON* root = cJSON_Parse(raw);
|
|
free(raw);
|
|
if (!root) continue;
|
|
|
|
double balance = 0.0;
|
|
if (extract_balance_value(root, &balance) == 0) {
|
|
fprintf(stderr, " Balance check: available balance %.4f\n", balance);
|
|
cJSON_Delete(root);
|
|
llm_cleanup();
|
|
return;
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
llm_cleanup();
|
|
fprintf(stderr, " Balance check: endpoint unavailable or unrecognized response (continuing)\n");
|
|
}
|
|
|
|
static int prompt_model_selection(const cJSON* models,
|
|
const char* default_model,
|
|
char* out_model,
|
|
size_t out_model_size) {
|
|
if (!out_model || out_model_size == 0 || !default_model || default_model[0] == '\0') return -1;
|
|
|
|
char filter[WIZARD_LINE_MAX] = {0};
|
|
for (;;) {
|
|
int total = (models && cJSON_IsArray(models)) ? cJSON_GetArraySize((cJSON*)models) : 0;
|
|
int matched_count = 0;
|
|
int matched_idx[256] = {0};
|
|
|
|
for (int i = 0; i < total && matched_count < (int)(sizeof(matched_idx) / sizeof(matched_idx[0])); i++) {
|
|
cJSON* it = cJSON_GetArrayItem((cJSON*)models, i);
|
|
if (!it || !cJSON_IsString(it) || !it->valuestring) continue;
|
|
if (strcasestr_like(it->valuestring, filter)) {
|
|
matched_idx[matched_count++] = i;
|
|
}
|
|
}
|
|
|
|
fprintf(stderr, "\n Model Search\n");
|
|
fprintf(stderr, " Filter [%s] Matches: %d/%d\n", filter[0] ? filter : "(none)", matched_count, total);
|
|
int show = matched_count < 20 ? matched_count : 20;
|
|
for (int i = 0; i < show; i++) {
|
|
cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[i]);
|
|
fprintf(stderr, " %d. %s\n", i + 1, it->valuestring);
|
|
}
|
|
if (matched_count > show) {
|
|
fprintf(stderr, " ... (%d more matches)\n", matched_count - show);
|
|
}
|
|
|
|
fprintf(stderr,
|
|
"\n Enter a number to choose, '/text' to set filter, exact model id for manual entry,\n"
|
|
" or press Enter for default [%s]: ",
|
|
default_model);
|
|
|
|
char input[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt(NULL, input, sizeof(input)) != 0) return -1;
|
|
if (line_is_quit(input)) return -1;
|
|
|
|
if (input[0] == '\0') {
|
|
snprintf(out_model, out_model_size, "%s", default_model);
|
|
return 0;
|
|
}
|
|
|
|
if (input[0] == '/') {
|
|
snprintf(filter, sizeof(filter), "%s", input + 1);
|
|
continue;
|
|
}
|
|
|
|
int n = atoi(input);
|
|
if (n > 0 && n <= show) {
|
|
cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[n - 1]);
|
|
if (it && cJSON_IsString(it) && it->valuestring) {
|
|
snprintf(out_model, out_model_size, "%s", it->valuestring);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
snprintf(out_model, out_model_size, "%s", input);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg,
|
|
const char* step_title,
|
|
const char* section_title) {
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
|
return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator");
|
|
}
|
|
|
|
static int prompt_llm_config_with_header(didactyl_config_t* cfg,
|
|
const char* step_title,
|
|
const char* section_title) {
|
|
for (;;) {
|
|
render_wizard_page_header(step_title, section_title);
|
|
print_option('1', " ppq.ai");
|
|
print_option('2', " Routstr");
|
|
print_option('3', " OpenAI-compatible");
|
|
print_option('q', "uit");
|
|
wizard_option_t provider_opts[] = {{'1', ""}, {'2', ""}, {'3', ""}, {'q', ""}};
|
|
char provider_choice = read_menu_choice(provider_opts, 4);
|
|
if (provider_choice == 'q' || provider_choice == '\0') return -1;
|
|
|
|
char provider[32] = {0};
|
|
char base_url[OW_MAX_URL_LEN] = {0};
|
|
if (provider_choice == '1') {
|
|
snprintf(provider, sizeof(provider), "%s", "ppq");
|
|
snprintf(base_url, sizeof(base_url), "%s", "https://api.ppq.ai");
|
|
} else if (provider_choice == '2') {
|
|
snprintf(provider, sizeof(provider), "%s", "routstr");
|
|
snprintf(base_url, sizeof(base_url), "%s", "https://api.routstr.com");
|
|
} else {
|
|
snprintf(provider, sizeof(provider), "%s", "openai");
|
|
const char* default_base_url = cfg->llm.base_url[0] ? cfg->llm.base_url : "https://api.openai.com/v1";
|
|
char prompt[256];
|
|
snprintf(prompt, sizeof(prompt), " Base URL [%.200s]: ", default_base_url);
|
|
if (read_line_prompt(prompt, base_url, sizeof(base_url)) != 0) return -1;
|
|
if (line_is_quit(base_url)) return -1;
|
|
if (base_url[0] == '\0') {
|
|
snprintf(base_url, sizeof(base_url), "%s", default_base_url);
|
|
}
|
|
}
|
|
|
|
char api_key[OW_MAX_KEY_LEN] = {0};
|
|
if (read_secret_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;
|
|
snprintf(probe.provider, sizeof(probe.provider), "%s", provider);
|
|
snprintf(probe.base_url, sizeof(probe.base_url), "%s", base_url);
|
|
snprintf(probe.api_key, sizeof(probe.api_key), "%s", api_key);
|
|
if (probe.model[0] == '\0') {
|
|
snprintf(probe.model, sizeof(probe.model), "%s", "claude-haiku-4.5");
|
|
}
|
|
|
|
wizard_check_provider_balance(&probe);
|
|
|
|
cJSON* models = NULL;
|
|
if (fetch_models_for_llm_config(&probe, &models) != 0 || !models || !cJSON_IsArray(models)) {
|
|
fprintf(stderr, "%sModel discovery failed (GET /models).%s\n", ANSI_RED, ANSI_RESET);
|
|
print_option('r', "e-enter provider settings");
|
|
print_option('m', "anually enter model and continue");
|
|
print_option('q', "uit");
|
|
wizard_option_t fail_opts[] = {{'r', ""}, {'m', ""}, {'q', ""}};
|
|
char c = read_menu_choice(fail_opts, 3);
|
|
if (c == 'q' || c == '\0') {
|
|
cJSON_Delete(models);
|
|
return -1;
|
|
}
|
|
if (c == 'r') {
|
|
cJSON_Delete(models);
|
|
continue;
|
|
}
|
|
|
|
char model[OW_MAX_MODEL_LEN] = {0};
|
|
if (read_line_prompt(" Model [claude-haiku-4.5]: ", model, sizeof(model)) != 0) {
|
|
cJSON_Delete(models);
|
|
return -1;
|
|
}
|
|
if (line_is_quit(model)) {
|
|
cJSON_Delete(models);
|
|
return -1;
|
|
}
|
|
if (model[0] == '\0') {
|
|
snprintf(model, sizeof(model), "%s", "claude-haiku-4.5");
|
|
}
|
|
|
|
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
|
|
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
|
|
snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key);
|
|
snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model);
|
|
cfg->llm.max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
|
|
cfg->llm.temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;
|
|
cJSON_Delete(models);
|
|
return 0;
|
|
}
|
|
|
|
char model[OW_MAX_MODEL_LEN] = {0};
|
|
if (prompt_model_selection(models, "claude-haiku-4.5", model, sizeof(model)) != 0) {
|
|
cJSON_Delete(models);
|
|
return -1;
|
|
}
|
|
cJSON_Delete(models);
|
|
|
|
int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
|
|
double temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;
|
|
|
|
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
|
|
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
|
|
snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key);
|
|
snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model);
|
|
cfg->llm.max_tokens = max_tokens;
|
|
cfg->llm.temperature = temperature;
|
|
|
|
if (wizard_llm_test(&cfg->llm) == 0) {
|
|
fprintf(stderr, "%sLLM test: OK%s\n", ANSI_YELLOW, ANSI_RESET);
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "%sLLM test: FAILED%s\n\n", ANSI_RED, ANSI_RESET);
|
|
wizard_option_t opts[] = {
|
|
{'r', "e-enter LLM settings"},
|
|
{'s', "kip test and continue anyway"},
|
|
{'q', "uit"}
|
|
};
|
|
print_option('r', "e-enter LLM settings");
|
|
print_option('s', "kip test and continue anyway");
|
|
print_option('q', "uit");
|
|
|
|
char c = read_menu_choice(opts, 3);
|
|
if (c == 's') return 0;
|
|
if (c == 'q' || c == '\0') return -1;
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
static int prompt_relay_configuration_with_header(didactyl_config_t* cfg,
|
|
const char* step_title,
|
|
const char* section_title) {
|
|
char input[WIZARD_LINE_MAX];
|
|
|
|
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, "\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");
|
|
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
continue;
|
|
}
|
|
if (c == 'r') {
|
|
if (cfg->relay_count <= 1) {
|
|
fprintf(stderr, "%sCannot remove last relay.%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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (pid == 0) {
|
|
execvp(argv[0], argv);
|
|
_exit(127);
|
|
}
|
|
|
|
int status = 0;
|
|
if (waitpid(pid, &status, 0) < 0) return -1;
|
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int run_privileged_command_local(char* const argv[]) {
|
|
if (!argv || !argv[0]) return -1;
|
|
|
|
if (geteuid() == 0) {
|
|
return run_command_local(argv);
|
|
}
|
|
|
|
char* sudo_argv[64] = {0};
|
|
sudo_argv[0] = "sudo";
|
|
|
|
int i = 0;
|
|
for (; argv[i] && i < 62; i++) {
|
|
sudo_argv[i + 1] = argv[i];
|
|
}
|
|
|
|
if (argv[i] != NULL) {
|
|
return -1;
|
|
}
|
|
|
|
sudo_argv[i + 1] = NULL;
|
|
return run_command_local(sudo_argv);
|
|
}
|
|
|
|
static int write_file_privileged_local(const char* path, const char* content, unsigned int mode) {
|
|
if (!path || path[0] == '\0' || !content) return -1;
|
|
|
|
char tmp_template[] = "/tmp/didactyl-wizard-XXXXXX";
|
|
int fd = mkstemp(tmp_template);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
|
|
FILE* fp = fdopen(fd, "w");
|
|
if (!fp) {
|
|
close(fd);
|
|
unlink(tmp_template);
|
|
return -1;
|
|
}
|
|
|
|
size_t len = strlen(content);
|
|
int ok = (fwrite(content, 1, len, fp) == len) ? 1 : 0;
|
|
if (fclose(fp) != 0) {
|
|
ok = 0;
|
|
}
|
|
|
|
if (!ok) {
|
|
unlink(tmp_template);
|
|
return -1;
|
|
}
|
|
|
|
char mode_buf[16] = {0};
|
|
snprintf(mode_buf, sizeof(mode_buf), "%04o", mode & 07777U);
|
|
|
|
char* install_argv[] = {"install", "-m", mode_buf, tmp_template, (char*)path, NULL};
|
|
int rc = run_privileged_command_local(install_argv);
|
|
|
|
unlink(tmp_template);
|
|
return rc;
|
|
}
|
|
|
|
static void sanitize_service_user_from_name(const char* name, char* out, size_t out_size) {
|
|
if (!out || out_size == 0) return;
|
|
out[0] = '\0';
|
|
|
|
const char* src = (name && name[0] != '\0') ? name : "didactyl";
|
|
size_t j = 0;
|
|
for (size_t i = 0; src[i] != '\0' && j + 1 < out_size; i++) {
|
|
char c = (char)tolower((unsigned char)src[i]);
|
|
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
|
out[j++] = c;
|
|
} else if ((c == ' ' || c == '_' || c == '-') && j > 0 && out[j - 1] != '-') {
|
|
out[j++] = '-';
|
|
}
|
|
if (j >= 31) break;
|
|
}
|
|
|
|
while (j > 0 && out[j - 1] == '-') j--;
|
|
out[j] = '\0';
|
|
|
|
if (out[0] == '\0' || !(out[0] >= 'a' && out[0] <= 'z')) {
|
|
snprintf(out, out_size, "didactyl");
|
|
}
|
|
}
|
|
|
|
|
|
static const char* detect_ca_bundle_path_local(void) {
|
|
const char* env = getenv("SSL_CERT_FILE");
|
|
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
|
return env;
|
|
}
|
|
static const char* candidates[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
"/etc/ssl/cert.pem",
|
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
"/etc/ssl/ca-bundle.pem"
|
|
};
|
|
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
|
if (access(candidates[i], R_OK) == 0) {
|
|
return candidates[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int write_sudoers_file(const char* service_user) {
|
|
if (!service_user || service_user[0] == '\0') return -1;
|
|
|
|
char sudoers_path[PATH_MAX] = {0};
|
|
snprintf(sudoers_path, sizeof(sudoers_path), "/etc/sudoers.d/%s", service_user);
|
|
|
|
char content[4096] = {0};
|
|
int n = snprintf(content,
|
|
sizeof(content),
|
|
"# Didactyl agent — scoped system maintenance privileges\n"
|
|
"# Generated by didactyl setup wizard\n"
|
|
"\n"
|
|
"# Service management\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload\n"
|
|
"\n"
|
|
"# Log inspection\n"
|
|
"%s ALL=(ALL) NOPASSWD: /usr/bin/journalctl *\n",
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
service_user);
|
|
if (n < 0 || (size_t)n >= sizeof(content)) {
|
|
fprintf(stderr, "%sFailed to build sudoers content for %s.%s\n", ANSI_RED, service_user, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
if (write_file_privileged_local(sudoers_path, content, 0440U) != 0) {
|
|
fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int install_system_service_with_dedicated_user(const didactyl_config_t* cfg, const char* agent_name) {
|
|
/* local mode requires an nsec to embed in ExecStart; remote signer modes
|
|
do not hold the nsec and instead pass --signer flags. */
|
|
if (!cfg) return -1;
|
|
if (strcmp(cfg->signer.mode, "local") == 0 && cfg->keys.nsec[0] == '\0') return -1;
|
|
|
|
if (geteuid() != 0) {
|
|
fprintf(stderr, " Requesting sudo authentication for system install...\n");
|
|
char* sudo_validate_argv[] = {"sudo", "-v", NULL};
|
|
if (run_command_local(sudo_validate_argv) != 0) {
|
|
fprintf(stderr, "%sSudo authentication failed. System install canceled.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* --- Derive service username from agent name --- */
|
|
char service_user[64] = {0};
|
|
sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
|
|
|
|
/* --- Create dedicated user if it doesn't exist --- */
|
|
struct passwd* pw = getpwnam(service_user);
|
|
if (!pw) {
|
|
fprintf(stderr, " Creating system user '%s'...\n", service_user);
|
|
char comment[128] = {0};
|
|
snprintf(comment, sizeof(comment), "Didactyl Nostr Agent (%s)", agent_name);
|
|
char* useradd_argv[] = {"useradd", "-m", "-s", "/bin/bash", "-c", comment, service_user, NULL};
|
|
if (run_privileged_command_local(useradd_argv) != 0) {
|
|
fprintf(stderr, "%sFailed to create system user '%s'.%s\n", ANSI_RED, service_user, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
pw = getpwnam(service_user);
|
|
if (!pw) {
|
|
fprintf(stderr, "%sCreated user but failed to resolve passwd entry.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
} else {
|
|
fprintf(stderr, " User '%s' already exists, reusing.\n", service_user);
|
|
}
|
|
|
|
const char* home = (pw->pw_dir && pw->pw_dir[0] != '\0') ? pw->pw_dir : NULL;
|
|
if (!home) {
|
|
fprintf(stderr, "%sUnable to determine service user home directory.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
/* --- Copy binary --- */
|
|
char self_path[PATH_MAX] = {0};
|
|
ssize_t self_len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
|
|
if (self_len <= 0) {
|
|
fprintf(stderr, "%sUnable to resolve running binary path.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
self_path[self_len] = '\0';
|
|
|
|
char bin_path[PATH_MAX] = {0};
|
|
snprintf(bin_path, sizeof(bin_path), "%s/didactyl", home);
|
|
fprintf(stderr, " Copying binary to %s...\n", bin_path);
|
|
char* install_bin_argv[] = {"install", "-m", "0755", self_path, bin_path, NULL};
|
|
if (run_privileged_command_local(install_bin_argv) != 0) {
|
|
fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
/* --- Set ownership on home directory and installed binary --- */
|
|
char owner_spec[64] = {0};
|
|
snprintf(owner_spec, sizeof(owner_spec), "%lu:%lu", (unsigned long)pw->pw_uid, (unsigned long)pw->pw_gid);
|
|
|
|
char* chown_home_argv[] = {"chown", owner_spec, (char*)home, NULL};
|
|
char* chmod_home_argv[] = {"chmod", "0750", (char*)home, NULL};
|
|
char* chown_bin_argv[] = {"chown", owner_spec, bin_path, NULL};
|
|
|
|
if (run_privileged_command_local(chown_home_argv) != 0 ||
|
|
run_privileged_command_local(chmod_home_argv) != 0 ||
|
|
run_privileged_command_local(chown_bin_argv) != 0) {
|
|
fprintf(stderr, "%sFailed to set ownership/permissions for installed files.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
/* --- Write sudoers file for scoped sudo privileges --- */
|
|
fprintf(stderr, " Writing sudoers file for '%s'...\n", service_user);
|
|
if (write_sudoers_file(service_user) != 0) {
|
|
fprintf(stderr, "%sWarning: Failed to write sudoers file. Agent will not have sudo access.%s\n",
|
|
ANSI_YELLOW, ANSI_RESET);
|
|
/* Non-fatal — agent can still run without sudo */
|
|
}
|
|
|
|
/* --- Detect CA bundle for TLS --- */
|
|
const char* ca_bundle = detect_ca_bundle_path_local();
|
|
|
|
/* --- Write systemd unit file --- */
|
|
char service_name[96] = {0};
|
|
snprintf(service_name, sizeof(service_name), "%s.service", service_user);
|
|
|
|
char service_path[PATH_MAX] = {0};
|
|
snprintf(service_path, sizeof(service_path), "/etc/systemd/system/%s", service_name);
|
|
fprintf(stderr, " Writing systemd unit to %s...\n", service_path);
|
|
|
|
char env_line[PATH_MAX + 64] = {0};
|
|
if (ca_bundle) {
|
|
snprintf(env_line, sizeof(env_line), "Environment=SSL_CERT_FILE=%s\\n", ca_bundle);
|
|
}
|
|
|
|
/* 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_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};
|
|
if (strcmp(cfg->signer.mode, "nsigner_unix") == 0) {
|
|
snprintf(key_args, sizeof(key_args),
|
|
"--signer nsigner_unix --signer-role %s --signer-timeout %d%s%s",
|
|
cfg->signer.role[0] ? cfg->signer.role : "main",
|
|
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 : "");
|
|
} 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);
|
|
snprintf(key_args, sizeof(key_args),
|
|
"--signer-tcp %s --signer-role %s --signer-timeout %d",
|
|
host_port,
|
|
cfg->signer.role[0] ? cfg->signer.role : "main",
|
|
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000);
|
|
} 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",
|
|
cfg->signer.serial_device,
|
|
cfg->signer.role[0] ? cfg->signer.role : "main",
|
|
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000);
|
|
} 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),
|
|
"--signer nsigner_qrexec --signer-qrexec %s --signer-service %s --signer-role %s --signer-timeout %d",
|
|
cfg->signer.target_qube,
|
|
svc,
|
|
cfg->signer.role[0] ? cfg->signer.role : "main",
|
|
cfg->signer.timeout_ms > 0 ? cfg->signer.timeout_ms : 15000);
|
|
} 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 "
|
|
"(file descriptors are runtime-only).%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
} else {
|
|
/* local mode (default): embed the nsec directly in ExecStart. */
|
|
snprintf(key_args, sizeof(key_args), "--nsec %s", cfg->keys.nsec);
|
|
}
|
|
|
|
char service_content[8192] = {0};
|
|
int svc_len = snprintf(service_content,
|
|
sizeof(service_content),
|
|
"[Unit]\n"
|
|
"Description=Didactyl Nostr Agent (%s)\n"
|
|
"After=network-online.target\n"
|
|
"Wants=network-online.target\n\n"
|
|
"[Service]\n"
|
|
"Type=notify\n"
|
|
"NotifyAccess=main\n"
|
|
"WatchdogSec=120\n"
|
|
"User=%s\n"
|
|
"Group=%s\n"
|
|
"WorkingDirectory=%s\n"
|
|
"ExecStart=%s --debug 3 %s\n"
|
|
"%s"
|
|
"SyslogIdentifier=%s\n"
|
|
"NoNewPrivileges=false\n"
|
|
"ProtectSystem=strict\n"
|
|
"ReadWritePaths=%s\n"
|
|
"ProtectHome=false\n"
|
|
"PrivateTmp=yes\n"
|
|
"Restart=on-failure\n"
|
|
"RestartSec=10\n"
|
|
"StandardOutput=journal\n"
|
|
"StandardError=journal\n\n"
|
|
"[Install]\n"
|
|
"WantedBy=multi-user.target\n",
|
|
service_user,
|
|
service_user,
|
|
service_user,
|
|
home,
|
|
bin_path,
|
|
key_args,
|
|
env_line,
|
|
service_user,
|
|
home);
|
|
if (svc_len < 0 || (size_t)svc_len >= sizeof(service_content)) {
|
|
fprintf(stderr, "%sFailed to build service file content.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
if (write_file_privileged_local(service_path, service_content, 0644U) != 0) {
|
|
fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
/* --- Enable and start the service --- */
|
|
fprintf(stderr, " Enabling and starting %s...\n", service_name);
|
|
char* reload_argv[] = {"systemctl", "daemon-reload", NULL};
|
|
char* enable_argv[] = {"systemctl", "enable", service_name, NULL};
|
|
char* start_argv[] = {"systemctl", "start", service_name, NULL};
|
|
|
|
if (run_privileged_command_local(reload_argv) != 0 ||
|
|
run_privileged_command_local(enable_argv) != 0 ||
|
|
run_privileged_command_local(start_argv) != 0) {
|
|
fprintf(stderr, "%sFailed to enable/start systemd service %s.%s\n", ANSI_RED, service_name, ANSI_RESET);
|
|
fprintf(stderr, " You can manually start with:\n");
|
|
fprintf(stderr, " sudo systemctl daemon-reload\n");
|
|
fprintf(stderr, " sudo systemctl enable %s\n", service_name);
|
|
fprintf(stderr, " sudo systemctl start %s\n", service_name);
|
|
return -1;
|
|
}
|
|
|
|
fprintf(stderr, "\n%sSystem service installed and started.%s\n", ANSI_YELLOW, ANSI_RESET);
|
|
fprintf(stderr, " Service: %s\n", service_name);
|
|
fprintf(stderr, " User: %s\n", service_user);
|
|
fprintf(stderr, " Home: %s\n", home);
|
|
fprintf(stderr, " Binary: %s\n", bin_path);
|
|
if (strcmp(cfg->signer.mode, "local") == 0) {
|
|
fprintf(stderr, " Nsec: embedded directly in ExecStart\n");
|
|
} else {
|
|
fprintf(stderr, " Signer: %s (nsec held by n_signer, not the agent)\n", cfg->signer.mode);
|
|
}
|
|
fprintf(stderr, " Sudoers: /etc/sudoers.d/%s\n", service_user);
|
|
fprintf(stderr, " Unit: %s\n", service_path);
|
|
fprintf(stderr, "\n Useful commands:\n");
|
|
fprintf(stderr, " sudo systemctl status %s\n", service_name);
|
|
fprintf(stderr, " sudo journalctl -u %s -f\n", service_name);
|
|
fprintf(stderr, " sudo systemctl restart %s\n", service_name);
|
|
return 0;
|
|
}
|
|
|
|
static __attribute__((unused)) int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
|
|
if (!path_out || path_out_size == 0) return -1;
|
|
char input[WIZARD_LINE_MAX];
|
|
if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1;
|
|
if (line_is_quit(input)) return -1;
|
|
if (input[0] == '\0') {
|
|
snprintf(path_out, path_out_size, "%s", "./genesis.jsonc");
|
|
} else {
|
|
strncpy(path_out, input, path_out_size - 1U);
|
|
path_out[path_out_size - 1U] = '\0';
|
|
}
|
|
|
|
if (access(path_out, F_OK) == 0) {
|
|
fprintf(stderr, "%sFile exists: %s%s\n", ANSI_YELLOW, path_out, ANSI_RESET);
|
|
print_option('o', "verwrite");
|
|
print_option('c', "hoose different path");
|
|
print_option('q', "uit");
|
|
wizard_option_t opts[] = {{'o', ""}, {'c', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 3);
|
|
if (c == 'o') return 0;
|
|
if (c == 'c') return maybe_prompt_write_path(path_out, path_out_size);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Parse a "read_fd:write_fd" pair string into two ints.
|
|
* Returns 0 on success, -1 on malformed input. */
|
|
static int wizard_parse_fd_pair(const char* s, int* read_fd_out, int* write_fd_out) {
|
|
if (!s || !read_fd_out || !write_fd_out) return -1;
|
|
const char* colon = strchr(s, ':');
|
|
if (!colon) return -1;
|
|
char read_buf[16] = {0};
|
|
size_t read_len = (size_t)(colon - s);
|
|
if (read_len == 0 || read_len >= sizeof(read_buf)) return -1;
|
|
memcpy(read_buf, s, read_len);
|
|
int r = atoi(read_buf);
|
|
int w = atoi(colon + 1);
|
|
if (r < 0 || w < 0) return -1;
|
|
*read_fd_out = r;
|
|
*write_fd_out = w;
|
|
return 0;
|
|
}
|
|
|
|
/* Prompt the operator to choose an n_signer transport and configure
|
|
* cfg->signer accordingly. Performs a connectivity check via
|
|
* nostr_signer_get_public_key and populates cfg->keys.public_key_hex.
|
|
*
|
|
* If signer_out is non-NULL, the successfully-constructed signer handle is
|
|
* returned to the caller (who becomes responsible for nostr_signer_free()).
|
|
* If signer_out is NULL, the ephemeral signer is freed here.
|
|
*
|
|
* Returns:
|
|
* 0 -> a transport was chosen and the connectivity check succeeded
|
|
* 1 -> operator chose "back"
|
|
* -1 -> operator chose "quit" / fatal error
|
|
*/
|
|
static int prompt_signer_transport(didactyl_config_t* cfg, nostr_signer_t** signer_out) {
|
|
if (!cfg) return -1;
|
|
if (signer_out) *signer_out = NULL;
|
|
|
|
for (;;) {
|
|
render_wizard_page_header("n_signer Transport", "Sign with a running n_signer");
|
|
print_option('u', "nix abstract socket (auto-discover or named)");
|
|
print_option('t', "cp (host:port + optional auth privkey)");
|
|
print_option('s', "erial USB device (auto-discover or path)");
|
|
print_option('f', "d pair (read_fd:write_fd -- advanced)");
|
|
print_option('e', "xec -- Qubes cross-qube qrexec (target qube + service)");
|
|
print_option('b', "ack");
|
|
print_option('q', "uit");
|
|
wizard_option_t opts[] = {{'u', ""}, {'t', ""}, {'s', ""}, {'f', ""}, {'e', ""}, {'b', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 7);
|
|
if (c == 'q') return -1;
|
|
if (c == 'b') return 1;
|
|
|
|
char chosen_mode[OW_MAX_SIGNER_MODE_LEN] = {0};
|
|
|
|
if (c == 'u') {
|
|
snprintf(chosen_mode, sizeof(chosen_mode), "%s", "nsigner_unix");
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
char names[16][64] = {{0}};
|
|
int found = nsigner_transport_list_unix(names, 16);
|
|
if (found > 0) {
|
|
fprintf(stderr, "\n Discovered n_signer abstract sockets:\n");
|
|
for (int i = 0; i < found; i++) {
|
|
fprintf(stderr, " %d. %s\n", i + 1, names[i]);
|
|
}
|
|
fprintf(stderr, "\n Pick a number, type a custom name, or press Enter for auto-discovery at boot.\n");
|
|
} else {
|
|
fprintf(stderr, "\n No running n_signer abstract sockets discovered.\n");
|
|
fprintf(stderr, " Type a socket name or press Enter for auto-discovery at boot.\n");
|
|
}
|
|
char input[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt("> ", input, sizeof(input)) != 0) return -1;
|
|
if (line_is_quit(input)) return -1;
|
|
if (input[0] == '\0') {
|
|
cfg->signer.socket_name[0] = '\0';
|
|
} else {
|
|
int n = atoi(input);
|
|
if (n > 0 && n <= found) {
|
|
snprintf(cfg->signer.socket_name, sizeof(cfg->signer.socket_name), "%s", names[n - 1]);
|
|
} else {
|
|
snprintf(cfg->signer.socket_name, sizeof(cfg->signer.socket_name), "%s", input);
|
|
}
|
|
}
|
|
#else
|
|
fprintf(stderr, "%sThis build was compiled without n_signer client support (NOSTR_ENABLE_NSIGNER_CLIENT).%s\n",
|
|
ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
#endif
|
|
}
|
|
|
|
if (c == 't') {
|
|
snprintf(chosen_mode, sizeof(chosen_mode), "%s", "nsigner_tcp");
|
|
char hp[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt(" host:port: ", hp, sizeof(hp)) != 0) return -1;
|
|
if (line_is_quit(hp)) return -1;
|
|
const char* colon = strchr(hp, ':');
|
|
if (!colon || hp[0] == '\0') {
|
|
fprintf(stderr, "%sInvalid host:port.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
size_t hlen = (size_t)(colon - hp);
|
|
if (hlen == 0 || hlen >= sizeof(cfg->signer.tcp_host)) {
|
|
fprintf(stderr, "%sInvalid host:port.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
memcpy(cfg->signer.tcp_host, hp, hlen);
|
|
cfg->signer.tcp_host[hlen] = '\0';
|
|
cfg->signer.tcp_port = atoi(colon + 1);
|
|
if (cfg->signer.tcp_port <= 0 || cfg->signer.tcp_port > 65535) {
|
|
fprintf(stderr, "%sInvalid port.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
char auth_hex[OW_MAX_SIGNER_AUTH_HEX_LEN] = {0};
|
|
if (read_secret_prompt(" Auth privkey hex (optional, for kind-27235 envelope): ",
|
|
auth_hex, sizeof(auth_hex)) != 0) return -1;
|
|
if (line_is_quit(auth_hex)) return -1;
|
|
if (auth_hex[0] != '\0') {
|
|
snprintf(cfg->signer.auth_privkey_hex, sizeof(cfg->signer.auth_privkey_hex), "%s", auth_hex);
|
|
} else {
|
|
cfg->signer.auth_privkey_hex[0] = '\0';
|
|
}
|
|
}
|
|
|
|
if (c == 's') {
|
|
snprintf(chosen_mode, sizeof(chosen_mode), "%s", "nsigner_serial");
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
char paths[16][64] = {{0}};
|
|
int found = nsigner_transport_list_serial(paths, 16);
|
|
if (found > 0) {
|
|
fprintf(stderr, "\n Discovered serial devices:\n");
|
|
for (int i = 0; i < found; i++) {
|
|
fprintf(stderr, " %d. %s\n", i + 1, paths[i]);
|
|
}
|
|
fprintf(stderr, "\n Pick a number, type a custom path, or press Enter to skip.\n");
|
|
} else {
|
|
fprintf(stderr, "\n No serial devices discovered.\n");
|
|
fprintf(stderr, " Type a device path (e.g. /dev/ttyACM0).\n");
|
|
}
|
|
char input[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt("> ", input, sizeof(input)) != 0) return -1;
|
|
if (line_is_quit(input)) return -1;
|
|
if (input[0] == '\0') {
|
|
fprintf(stderr, "%sA serial device path is required.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
int n = atoi(input);
|
|
if (n > 0 && n <= found) {
|
|
snprintf(cfg->signer.serial_device, sizeof(cfg->signer.serial_device), "%s", paths[n - 1]);
|
|
} else {
|
|
snprintf(cfg->signer.serial_device, sizeof(cfg->signer.serial_device), "%s", input);
|
|
}
|
|
#else
|
|
fprintf(stderr, "%sThis build was compiled without n_signer client support (NOSTR_ENABLE_NSIGNER_CLIENT).%s\n",
|
|
ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
#endif
|
|
}
|
|
|
|
if (c == 'f') {
|
|
snprintf(chosen_mode, sizeof(chosen_mode), "%s", "nsigner_fds");
|
|
char fds[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt(" read_fd:write_fd: ", fds, sizeof(fds)) != 0) return -1;
|
|
if (line_is_quit(fds)) return -1;
|
|
if (wizard_parse_fd_pair(fds, &cfg->signer.fds_read_fd, &cfg->signer.fds_write_fd) != 0) {
|
|
fprintf(stderr, "%sInvalid read_fd:write_fd.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (c == 'e') {
|
|
snprintf(chosen_mode, sizeof(chosen_mode), "%s", "nsigner_qrexec");
|
|
char qube[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt(" Target qube (e.g. nostr_signer): ", qube, sizeof(qube)) != 0) return -1;
|
|
if (line_is_quit(qube)) return -1;
|
|
if (qube[0] == '\0') {
|
|
fprintf(stderr, "%sA target qube name is required.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
snprintf(cfg->signer.target_qube, sizeof(cfg->signer.target_qube), "%s", qube);
|
|
char svc[WIZARD_LINE_MAX] = {0};
|
|
if (read_line_prompt(" qrexec service [qubes.NsignerRpc]: ", svc, sizeof(svc)) != 0) return -1;
|
|
if (line_is_quit(svc)) return -1;
|
|
snprintf(cfg->signer.service_name, sizeof(cfg->signer.service_name), "%s", svc[0] ? svc : "qubes.NsignerRpc");
|
|
}
|
|
|
|
/* Common: role + timeout. */
|
|
char role_buf[OW_MAX_SIGNER_ROLE_LEN] = {0};
|
|
if (read_line_prompt(" Role [main]: ", 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 : "main");
|
|
|
|
char timeout_buf[32] = {0};
|
|
if (read_line_prompt(" Timeout ms [15000]: ", timeout_buf, sizeof(timeout_buf)) != 0) return -1;
|
|
if (line_is_quit(timeout_buf)) return -1;
|
|
cfg->signer.timeout_ms = (timeout_buf[0] != '\0') ? atoi(timeout_buf) : 15000;
|
|
if (cfg->signer.timeout_ms <= 0) cfg->signer.timeout_ms = 15000;
|
|
|
|
snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", chosen_mode);
|
|
|
|
/* Connectivity check. */
|
|
fprintf(stderr, " Checking connectivity to n_signer (mode=%s)...\n", chosen_mode);
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
nostr_signer_t* signer = NULL;
|
|
if (strcmp(chosen_mode, "nsigner_unix") == 0) {
|
|
signer = nostr_signer_nsigner_unix(cfg->signer.socket_name[0] ? cfg->signer.socket_name : NULL,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(chosen_mode, "nsigner_tcp") == 0) {
|
|
signer = nostr_signer_nsigner_tcp(cfg->signer.tcp_host, cfg->signer.tcp_port,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
if (signer && cfg->signer.auth_privkey_hex[0] != '\0') {
|
|
unsigned char auth_key[32];
|
|
if (nostr_hex_to_bytes(cfg->signer.auth_privkey_hex, auth_key, 32) == 0) {
|
|
(void)nostr_signer_nsigner_set_auth(signer, auth_key, "didactyl");
|
|
}
|
|
}
|
|
} else if (strcmp(chosen_mode, "nsigner_serial") == 0) {
|
|
signer = nostr_signer_nsigner_serial(cfg->signer.serial_device,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(chosen_mode, "nsigner_fds") == 0) {
|
|
signer = nostr_signer_nsigner_fds(cfg->signer.fds_read_fd, cfg->signer.fds_write_fd,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(chosen_mode, "nsigner_qrexec") == 0) {
|
|
const char* svc = cfg->signer.service_name[0] ? cfg->signer.service_name : "qubes.NsignerRpc";
|
|
signer = nostr_signer_nsigner_qrexec(cfg->signer.target_qube, svc,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
}
|
|
|
|
if (!signer) {
|
|
fprintf(stderr, "%sFailed to initialize n_signer client (mode=%s).%s\n",
|
|
ANSI_RED, chosen_mode, ANSI_RESET);
|
|
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));
|
|
if (line_is_quit(pause_buf)) return -1;
|
|
continue;
|
|
}
|
|
|
|
char pubkey_hex[65] = {0};
|
|
if (nostr_signer_get_public_key(signer, pubkey_hex) != 0) {
|
|
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");
|
|
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));
|
|
if (line_is_quit(pause_buf)) return -1;
|
|
continue;
|
|
}
|
|
snprintf(cfg->keys.public_key_hex, sizeof(cfg->keys.public_key_hex), "%s", pubkey_hex);
|
|
fprintf(stderr, "%sConnectivity check: OK. Pubkey: %s%s\n",
|
|
ANSI_YELLOW, cfg->keys.public_key_hex, ANSI_RESET);
|
|
|
|
if (signer_out) {
|
|
*signer_out = signer;
|
|
} else {
|
|
nostr_signer_free(signer);
|
|
}
|
|
return 0;
|
|
#else
|
|
fprintf(stderr, "%sThis build was compiled without n_signer client support (NOSTR_ENABLE_NSIGNER_CLIENT). "
|
|
"Connectivity check skipped; the agent will fail at startup in this mode.%s\n",
|
|
ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* Construct an ephemeral nostr_signer_t* from cfg->signer for wizard publish/
|
|
* recovery paths. Returns NULL for local mode (caller uses raw-key path) or
|
|
* on construction failure. Caller must nostr_signer_free() the result. */
|
|
static nostr_signer_t* wizard_construct_ephemeral_signer(const didactyl_config_t* cfg) {
|
|
if (!cfg) return NULL;
|
|
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
|
const char* mode = cfg->signer.mode;
|
|
if (strcmp(mode, "nsigner_unix") == 0) {
|
|
return nostr_signer_nsigner_unix(cfg->signer.socket_name[0] ? cfg->signer.socket_name : NULL,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(mode, "nsigner_tcp") == 0) {
|
|
if (cfg->signer.tcp_host[0] == '\0' || cfg->signer.tcp_port <= 0) return NULL;
|
|
nostr_signer_t* s = nostr_signer_nsigner_tcp(cfg->signer.tcp_host, cfg->signer.tcp_port,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
if (s && cfg->signer.auth_privkey_hex[0] != '\0') {
|
|
unsigned char auth_key[32];
|
|
if (nostr_hex_to_bytes(cfg->signer.auth_privkey_hex, auth_key, 32) == 0) {
|
|
(void)nostr_signer_nsigner_set_auth(s, auth_key, "didactyl");
|
|
}
|
|
}
|
|
return s;
|
|
} else if (strcmp(mode, "nsigner_serial") == 0) {
|
|
if (cfg->signer.serial_device[0] == '\0') return NULL;
|
|
return nostr_signer_nsigner_serial(cfg->signer.serial_device,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(mode, "nsigner_fds") == 0) {
|
|
if (cfg->signer.fds_read_fd < 0 || cfg->signer.fds_write_fd < 0) return NULL;
|
|
return nostr_signer_nsigner_fds(cfg->signer.fds_read_fd, cfg->signer.fds_write_fd,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
} else if (strcmp(mode, "nsigner_qrexec") == 0) {
|
|
if (cfg->signer.target_qube[0] == '\0') return NULL;
|
|
const char* svc = cfg->signer.service_name[0] ? cfg->signer.service_name : "qubes.NsignerRpc";
|
|
return nostr_signer_nsigner_qrexec(cfg->signer.target_qube, svc,
|
|
cfg->signer.role, cfg->signer.timeout_ms);
|
|
}
|
|
#else
|
|
(void)cfg;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
static int new_agent_identity_step(didactyl_config_t* cfg) {
|
|
for (;;) {
|
|
render_wizard_page_header("Step 2 of 7", "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");
|
|
print_option('b', "ack");
|
|
print_option('q', "uit");
|
|
wizard_option_t opts[] = {{'g', ""}, {'p', ""}, {'s', ""}, {'b', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 5);
|
|
if (c == 'q') return -1;
|
|
if (c == 'b') return 1;
|
|
|
|
if (c == 'g') {
|
|
unsigned char priv[32];
|
|
unsigned char pub[32];
|
|
if (nostr_generate_keypair(priv, pub) != 0) {
|
|
fprintf(stderr, "%sFailed to generate keypair.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
memcpy(cfg->keys.private_key, priv, 32);
|
|
memcpy(cfg->keys.public_key, pub, 32);
|
|
nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex);
|
|
|
|
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;
|
|
}
|
|
snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec);
|
|
|
|
fprintf(stderr, "\nGenerated identity:\n");
|
|
fprintf(stderr, " npub: %s\n", npub);
|
|
fprintf(stderr, "%s nsec: %s%s\n", ANSI_YELLOW, nsec, ANSI_RESET);
|
|
fprintf(stderr, "%sSave this nsec now. It will not be persisted unless you explicitly export with nsec.%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;
|
|
}
|
|
|
|
if (c == 'p') {
|
|
char nsec_in[OW_MAX_KEY_LEN] = {0};
|
|
if (read_secret_prompt("Enter nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1;
|
|
if (line_is_quit(nsec_in)) return -1;
|
|
if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
|
|
fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
|
|
continue;
|
|
}
|
|
char npub[OW_MAX_KEY_LEN] = {0};
|
|
if (nostr_key_to_bech32(cfg->keys.public_key, "npub", npub) == 0) {
|
|
fprintf(stderr, "Derived identity: %s\n", npub);
|
|
} else {
|
|
fprintf(stderr, "Derived pubkey (hex): %s\n", cfg->keys.public_key_hex);
|
|
}
|
|
}
|
|
|
|
if (c == 's') {
|
|
int rc = prompt_signer_transport(cfg, NULL);
|
|
if (rc < 0) return -1;
|
|
if (rc == 1) continue; /* back -> re-render identity menu */
|
|
/* Connectivity check already populated cfg->keys.public_key_hex. */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t genesis_path_out_size) {
|
|
(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");
|
|
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);
|
|
if (rc != 0) return rc < 0 ? -1 : 1;
|
|
|
|
if (prompt_admin_pubkey(cfg) != 0) return -1;
|
|
|
|
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);
|
|
} 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");
|
|
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, "\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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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");
|
|
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);
|
|
fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url);
|
|
fprintf(stderr, " Relays: %d configured\n", cfg->relay_count);
|
|
fprintf(stderr, " DM Protocol: nip04\n\n");
|
|
|
|
print_option('b', "oot the agent now");
|
|
print_option('i', "nstall dedicated-user systemd service and boot");
|
|
print_option('s', "tart over");
|
|
print_option('q', "uit");
|
|
|
|
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 == '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);
|
|
}
|
|
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);
|
|
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",
|
|
ANSI_RED, ANSI_RESET);
|
|
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);
|
|
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;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int existing_agent_flow(didactyl_config_t* cfg) {
|
|
char nsec_in[OW_MAX_KEY_LEN] = {0};
|
|
char agent_name[OW_MAX_NAME_LEN] = {0};
|
|
int agent_name_found = 0;
|
|
int relay_changed = 0;
|
|
/* 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;
|
|
|
|
render_wizard_page_header("Existing Agent", "Identity");
|
|
print_option('n', "sec -- enter the agent private key");
|
|
print_option('s', "ign with a running n_signer (no nsec required)");
|
|
print_option('q', "uit");
|
|
wizard_option_t id_opts[] = {{'n', ""}, {'s', ""}, {'q', ""}};
|
|
char idc = read_menu_choice(id_opts, 3);
|
|
if (idc == 'q') return -1;
|
|
|
|
if (idc == 's') {
|
|
int rc = prompt_signer_transport(cfg, &flow_signer);
|
|
if (rc < 0) {
|
|
return -1;
|
|
}
|
|
if (rc == 1) {
|
|
/* back -> treat as quit to main menu */
|
|
return -1;
|
|
}
|
|
/* cfg->keys.public_key_hex populated by the connectivity check. */
|
|
} else {
|
|
if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) {
|
|
return -1;
|
|
}
|
|
if (line_is_quit(nsec_in)) return -1;
|
|
if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
|
|
fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int relay_found = 0;
|
|
if (recover_existing_config_from_nostr(cfg, &relay_found) != 0 || !relay_found) {
|
|
render_wizard_page_header("Existing Agent", "Config Recovery");
|
|
fprintf(stderr, " Relay list (kind 10002): NOT FOUND\n\n");
|
|
fprintf(stderr, "%sUnable to find a kind 10002 relay list for this identity.%s\n", ANSI_RED, ANSI_RESET);
|
|
fprintf(stderr, "\n");
|
|
print_option('n', "ew agent setup with this identity");
|
|
print_option('q', "uit to main menu");
|
|
wizard_option_t opts[] = {{'n', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 2);
|
|
if (c == 'n') {
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
int rc = new_agent_flow(cfg, NULL, 0);
|
|
if (rc == 0) return SETUP_WIZARD_RC_BOOTSTRAP;
|
|
if (rc == 1) return SETUP_WIZARD_RC_EXIT;
|
|
return -1;
|
|
}
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return -1;
|
|
}
|
|
|
|
if (recover_full_config_from_nostr(cfg, flow_signer, agent_name, sizeof(agent_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 || agent_name[0] == '\0') {
|
|
snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
|
|
}
|
|
|
|
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("Existing Agent", "Recovered Configuration");
|
|
fprintf(stderr, "Your agent was found. Change any of the following:\n\n");
|
|
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('a', "dmin pubkey");
|
|
print_option('l', "lm config");
|
|
print_option('r', "elay configuration");
|
|
print_option('c', "ontinue to launch options");
|
|
print_option('q', "uit");
|
|
|
|
wizard_option_t opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'c', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 5);
|
|
if (c == 'q') {
|
|
return -1;
|
|
}
|
|
if (c == 'c') {
|
|
break;
|
|
}
|
|
if (c == 'a') {
|
|
if (prompt_admin_pubkey_with_header(cfg,
|
|
"Existing Agent -- Review",
|
|
"Existing Agent -- Administrator") != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (c == 'l') {
|
|
if (prompt_llm_config_with_header(cfg,
|
|
"Existing Agent -- Review",
|
|
"Existing Agent -- LLM Provider") != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (c == 'r') {
|
|
for (;;) {
|
|
int rc = prompt_relay_configuration_with_header(cfg,
|
|
"Existing Agent -- Review",
|
|
"Existing Agent -- Relay Configuration");
|
|
if (rc < 0) return -1;
|
|
if (rc == 1) break;
|
|
relay_changed = 1;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
render_wizard_page_header("Existing Agent", "Launch Options");
|
|
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);
|
|
fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url);
|
|
fprintf(stderr, " Relays: %d configured\n\n", cfg->relay_count);
|
|
|
|
print_option('b', "oot the agent now");
|
|
print_option('i', "nstall dedicated-user systemd service and boot");
|
|
print_option('q', "uit");
|
|
|
|
wizard_option_t launch_opts[] = {{'b', ""}, {'i', ""}, {'q', ""}};
|
|
char lc = read_menu_choice(launch_opts, 3);
|
|
if (lc == 'q') return -1;
|
|
|
|
if (lc == 'b') {
|
|
if (relay_changed) {
|
|
if (ensure_startup_kind10002_from_current_relays(cfg) != 0) {
|
|
fprintf(stderr,
|
|
"%sFailed to stage updated relay list for startup publish.%s\n",
|
|
ANSI_RED,
|
|
ANSI_RESET);
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return -1;
|
|
}
|
|
}
|
|
/* Always return BOOTSTRAP so main() re-publishes kind 30078.
|
|
The wizard has the authoritative config in memory. */
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return SETUP_WIZARD_RC_BOOTSTRAP;
|
|
}
|
|
|
|
if (lc == '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",
|
|
ANSI_RED, ANSI_RESET);
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return -1;
|
|
}
|
|
|
|
/* Always publish kind 30078 before installing the service, even if
|
|
config was not changed. The service depends on recovering these
|
|
from relays and the event may have been purged. */
|
|
if (persist_runtime_config_to_nostr_wizard_online(cfg, flow_signer) != 0) {
|
|
fprintf(stderr,
|
|
"%sFailed to publish runtime config to Nostr; refusing install to avoid stale admin/LLM on first boot.%s\n",
|
|
ANSI_RED,
|
|
ANSI_RESET);
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return -1;
|
|
}
|
|
if (relay_changed) {
|
|
if (publish_kind10002_relays_wizard_online(cfg) != 0) {
|
|
fprintf(stderr,
|
|
"%sFailed to publish updated relay list (kind 10002) to Nostr; refusing install.%s\n",
|
|
ANSI_RED,
|
|
ANSI_RESET);
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
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,
|
|
"%sService %s is active. Setup is complete; this wizard will now exit.%s\n",
|
|
ANSI_YELLOW,
|
|
service_name,
|
|
ANSI_RESET);
|
|
} else {
|
|
fprintf(stderr,
|
|
"%sService installed but not active yet. Check status with: sudo systemctl status %s%s\n",
|
|
ANSI_RED,
|
|
service_name,
|
|
ANSI_RESET);
|
|
}
|
|
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return SETUP_WIZARD_RC_EXIT;
|
|
}
|
|
|
|
if (flow_signer) nostr_signer_free(flow_signer);
|
|
return -1;
|
|
}
|
|
|
|
static int load_genesis_path_flow(didactyl_config_t* cfg) {
|
|
char path[OW_MAX_URL_LEN] = {0};
|
|
render_wizard_page_header("Load Genesis File", "Bootstrap from File");
|
|
if (read_line_prompt("Path [./genesis.jsonc]: ", path, sizeof(path)) != 0) {
|
|
return -1;
|
|
}
|
|
if (line_is_quit(path)) return -1;
|
|
if (path[0] == '\0') {
|
|
snprintf(path, sizeof(path), "%s", "./genesis.jsonc");
|
|
}
|
|
|
|
config_free(cfg);
|
|
if (config_load(path, cfg) != 0) {
|
|
fprintf(stderr, "%sFailed to load %s: %s%s\n", ANSI_RED, path, config_last_error(), ANSI_RESET);
|
|
return -1;
|
|
}
|
|
|
|
if (cfg->keys.nsec[0] == '\0') {
|
|
char nsec_in[OW_MAX_KEY_LEN] = {0};
|
|
if (read_secret_prompt("Config has no nsec. Enter nsec now: ", nsec_in, sizeof(nsec_in)) != 0) return -1;
|
|
if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
|
|
fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t genesis_path_out_size) {
|
|
if (!config) return -1;
|
|
|
|
config_set_defaults(config);
|
|
if (set_default_relays(config) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
for (;;) {
|
|
render_wizard_page_header("Main Menu", "Choose Setup Mode");
|
|
print_option('n', "ew agent -- create a fresh identity");
|
|
print_option('e', "xisting -- start an already set up agent");
|
|
print_option('l', "oad -- boot from genesis.jsonc");
|
|
print_option('q', "uit");
|
|
|
|
wizard_option_t opts[] = {{'n', ""}, {'e', ""}, {'l', ""}, {'q', ""}};
|
|
char c = read_menu_choice(opts, 4);
|
|
if (c == 'q' || c == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
if (c == 'l') {
|
|
int rc = load_genesis_path_flow(config);
|
|
if (rc == 0) {
|
|
return SETUP_WIZARD_RC_BOOTSTRAP;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
if (c == 'e') {
|
|
int rc = existing_agent_flow(config);
|
|
if (rc == SETUP_WIZARD_RC_BOOTSTRAP ||
|
|
rc == SETUP_WIZARD_RC_EXISTING ||
|
|
rc == SETUP_WIZARD_RC_EXIT) {
|
|
return rc;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
if (c == 'n') {
|
|
int rc = new_agent_flow(config, genesis_path_out, genesis_path_out_size);
|
|
if (rc == 2) {
|
|
config_free(config);
|
|
config_set_defaults(config);
|
|
if (set_default_relays(config) != 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (rc == 0) {
|
|
return SETUP_WIZARD_RC_BOOTSTRAP;
|
|
}
|
|
if (rc == 1) {
|
|
return SETUP_WIZARD_RC_EXIT;
|
|
}
|
|
return rc;
|
|
}
|
|
}
|
|
}
|