v0.0.80 - Fix wizard/runtime config persistence and add --admin CLI override for agent_config updates

This commit is contained in:
Your Name
2026-03-18 11:21:57 -04:00
parent 7675109afc
commit 82362e4f3f
5 changed files with 327 additions and 24 deletions

View File

@@ -55,11 +55,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.79 ## Current Status — v0.0.80
**Active build — this project is barely working. Experiment at your own risk.** **Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.79Enhance existing-agent wizard with full config recovery review edit flow and new-agent fallback > Last release update: v0.0.80Fix wizard/runtime config persistence and add --admin CLI override for agent_config updates
- Connects to configured relays with auto-reconnect and relay state transition logging - Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected - Publishes configured startup events per relay as each relay becomes connected

View File

@@ -0,0 +1,69 @@
# Fix: Kind 30078 `d=agent_config` Publish Gaps
## Problem
The agent stores its admin pubkey and DM protocol in a NIP-44 self-encrypted kind 30078 replaceable event (`d=agent_config`). This event is critical for the `--nsec`-only startup path (used by systemd services) to recover the admin identity.
**Multiple startup paths fail to publish this event**, causing:
- Agents installed via wizard "Install Service" to lose their admin pubkey on restart
- Agents recovering on new servers to use stale/old admin pubkeys from relays
- Agents with genesis files to silently ignore newer kind 30078 values
## Affected Files
- `src/setup_wizard.c` — wizard flows
- `src/main.c` — main startup logic
## Fixes
### Fix A: `new_agent_flow` — publish before service install
**File:** `src/setup_wizard.c` ~line 2184
**Problem:** When user chooses "Install systemd service" in new agent flow, `persist_runtime_config_to_nostr_wizard_online` is never called. The service starts with `--nsec` only and has no kind 30078 to recover from.
**Fix:** Call `persist_runtime_config_to_nostr_wizard_online(cfg)` before `install_system_service_with_dedicated_user()`. Fail the install if publish fails (same pattern as existing_agent_flow).
### Fix B: `new_agent_flow` — publish before "boot now" return
**File:** `src/setup_wizard.c` ~line 2177
**Problem:** When user chooses "Boot now", the wizard returns `SETUP_WIZARD_RC_BOOTSTRAP` and relies on `main()` to publish later. This works but is fragile — if the bootstrap publish in `main()` fails, the kind 30078 is never created.
**Fix:** Call `persist_runtime_config_to_nostr_wizard_online(cfg)` before returning 0. This is belt-and-suspenders — `main()` will also publish, but the wizard ensures it happens at least once. If the wizard publish fails, log a warning but continue (non-fatal since main will retry).
### Fix C: `existing_agent_flow` — always publish before service install
**File:** `src/setup_wizard.c` ~line 2368
**Problem:** `persist_runtime_config_to_nostr_wizard_online` is only called `if (config_changed)`. If the user accepts the recovered config as-is, the kind 30078 is not re-published. The event may have been lost from relays.
**Fix:** Remove the `config_changed` gate. Always call `persist_runtime_config_to_nostr_wizard_online(cfg)` before installing the service. Kind 30078 is a replaceable event, so re-publishing is safe and idempotent.
### Fix D: `existing_agent_flow` — always return BOOTSTRAP
**File:** `src/setup_wizard.c` ~line 2364
**Problem:** When user chooses "Boot now" and `config_changed == 0`, the flow returns `SETUP_WIZARD_RC_EXISTING` which means `bootstrap_mode = 0` in `main()`. If `first_run = 0` (kind 10002 exists), `persist_runtime_config_to_nostr` is skipped.
**Fix:** Always return `SETUP_WIZARD_RC_BOOTSTRAP` from the "boot now" path, regardless of `config_changed`. This ensures `main()` always re-publishes the kind 30078. The wizard has all the config in memory — it should always be treated as authoritative.
### Fix E: `persist_runtime_config_to_nostr_wizard_online` — ensure relay delivery
**File:** `src/setup_wizard.c` ~line 1159
**Problem:** The function publishes the event and immediately calls `nostr_handler_cleanup()`. The event may not have been delivered to relays yet (fire-and-forget).
**Fix:** Add a brief poll loop (e.g., 2-3 seconds of `nostr_handler_poll()`) after the publish call and before `nostr_handler_cleanup()` to give the event time to propagate. This matches the pattern used elsewhere for relay operations.
### Fix F: `main()` — always fetch kind 30078 and compare
**File:** `src/main.c` ~line 847 (`recover_missing_runtime_config_from_nostr`)
**Problem:** The recovery only runs when `admin_config_is_complete()` returns false. If a genesis file provides an admin pubkey, the kind 30078 is never checked, even if it has a different (newer) value.
**Fix:** Always fetch the kind 30078 `d=agent_config` regardless of whether the genesis already provided values. Compare the fetched admin_pubkey with the loaded one. If they differ, log a `DEBUG_WARN` with both values. Genesis wins (operator intent), but the warning makes the conflict visible in logs.
### Fix G: `main()` — always re-publish kind 30078
**File:** `src/main.c` ~line 1152
**Problem:** `persist_runtime_config_to_nostr` only runs when `bootstrap_mode || first_run`. On subsequent runs, the kind 30078 is never refreshed. If relays purge the event, it's gone.
**Fix:** Move `persist_runtime_config_to_nostr(&cfg)` outside the `if (bootstrap_mode || first_run)` block so it runs on every startup. Kind 30078 is a replaceable event — re-publishing is safe, idempotent, and ensures relay persistence. Log the result but don't fail startup if it doesn't succeed.
## Execution Order
1. Fix E first (relay delivery) — this makes all other wizard publishes more reliable
2. Fixes A + B (new_agent_flow) — the most critical bug
3. Fixes C + D (existing_agent_flow) — second most critical
4. Fix G (always re-publish in main) — ensures long-term relay persistence
5. Fix F (conflict detection) — nice-to-have logging improvement
## Testing
- Wizard → New Agent → Install Service: verify kind 30078 exists on relays after service starts
- Wizard → New Agent → Boot: verify kind 30078 exists on relays
- Wizard → Existing Agent → Install Service (no changes): verify kind 30078 re-published
- Wizard → Existing Agent → Boot (no changes): verify kind 30078 re-published
- `--nsec` only restart: verify kind 30078 recovered and re-published
- `--config genesis.jsonc` with different admin than kind 30078: verify warning logged, genesis wins, kind 30078 updated

View File

@@ -75,6 +75,8 @@ static void print_usage(const char* prog) {
" If file is missing, startup falls back to minimal nsec-only mode.\n" " If file is missing, startup falls back to minimal nsec-only mode.\n"
" --nsec <nsec_or_hex>\n" " --nsec <nsec_or_hex>\n"
" Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n" " Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n"
" --admin <npub_or_hex>\n"
" Administrator pubkey override; accepts npub1... or 64-char hex.\n"
" --api-port <port>\n" " --api-port <port>\n"
" Enable HTTP API and bind to this port (1-65535).\n" " Enable HTTP API and bind to this port (1-65535).\n"
" --api-bind <addr>\n" " --api-bind <addr>\n"
@@ -863,24 +865,32 @@ static void recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
free(llm_plaintext); free(llm_plaintext);
} }
if (!admin_config_is_complete(cfg)) { char* agent_plaintext = NULL;
char* agent_plaintext = NULL; if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) { didactyl_config_t recalled = *cfg;
if (apply_recalled_agent_config(cfg, agent_plaintext) == 0) { if (apply_recalled_agent_config(&recalled, agent_plaintext) == 0) {
if (!admin_config_is_complete(cfg)) {
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", recalled.admin.pubkey);
cfg->dm_protocol = recalled.dm_protocol;
DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr"); DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr");
} else { } else if (recalled.admin.pubkey[0] != '\0' && strcmp(cfg->admin.pubkey, recalled.admin.pubkey) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled agent_config"); DEBUG_WARN("[didactyl] startup phase: agent_config admin pubkey differs from loaded config (loaded=%.16s..., nostr=%.16s...); loaded config wins",
cfg->admin.pubkey,
recalled.admin.pubkey);
} }
} else { } else {
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable"); DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled agent_config");
} }
free(agent_plaintext); } else {
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
} }
free(agent_plaintext);
} }
int main(int argc, char** argv) { int main(int argc, char** argv) {
const char* config_path = "./genesis.jsonc"; const char* config_path = "./genesis.jsonc";
const char* cli_nsec = NULL; const char* cli_nsec = NULL;
const char* cli_admin_pubkey = NULL;
const char* api_bind_override = NULL; const char* api_bind_override = NULL;
int api_port_override = 0; int api_port_override = 0;
int debug_level = DEBUG_LEVEL_NONE; int debug_level = DEBUG_LEVEL_NONE;
@@ -944,6 +954,8 @@ int main(int argc, char** argv) {
debug_level = atoi(argv[++i]); debug_level = atoi(argv[++i]);
} else if (strcmp(argv[i], "--nsec") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--nsec") == 0 && i + 1 < argc) {
cli_nsec = argv[++i]; cli_nsec = argv[++i];
} else if (strcmp(argv[i], "--admin") == 0 && i + 1 < argc) {
cli_admin_pubkey = argv[++i];
} else if (strcmp(argv[i], "--api-port") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--api-port") == 0 && i + 1 < argc) {
api_port_override = atoi(argv[++i]); api_port_override = atoi(argv[++i]);
} else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) {
@@ -997,6 +1009,18 @@ int main(int argc, char** argv) {
} }
} }
if (cli_admin_pubkey && cli_admin_pubkey[0] != '\0') {
char decoded_admin[65] = {0};
if (decode_pubkey_hex_or_npub_local(cli_admin_pubkey, decoded_admin) != 0) {
fprintf(stderr, "Invalid admin pubkey provided via --admin (expected npub1... or 64-char hex)\n");
config_free(&cfg);
nostr_cleanup();
return 1;
}
snprintf(cfg.admin.pubkey, sizeof(cfg.admin.pubkey), "%s", decoded_admin);
DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey);
}
if (config_ensure_default_skill_startup_events(&cfg) != 0) { if (config_ensure_default_skill_startup_events(&cfg) != 0) {
fprintf(stderr, "Failed to synthesize startup events from default_skill\n"); fprintf(stderr, "Failed to synthesize startup events from default_skill\n");
config_free(&cfg); config_free(&cfg);
@@ -1124,7 +1148,7 @@ int main(int argc, char** argv) {
if (!admin_config_is_complete(&cfg)) { if (!admin_config_is_complete(&cfg)) {
startup_step_fail(4, "Validate admin config", "missing admin pubkey"); startup_step_fail(4, "Validate admin config", "missing admin pubkey");
fprintf(stderr, fprintf(stderr,
"Missing admin config: provide admin.pubkey in genesis or store d=agent_config via config_store\n"); "Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=agent_config via config_store\n");
nostr_handler_cleanup(); nostr_handler_cleanup();
config_free(&cfg); config_free(&cfg);
nostr_cleanup(); nostr_cleanup();
@@ -1153,17 +1177,19 @@ int main(int argc, char** argv) {
if (nostr_handler_reconcile_startup_events() != 0) { if (nostr_handler_reconcile_startup_events() != 0) {
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)"); DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
} }
if (persist_runtime_config_to_nostr(&cfg) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
} else {
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
}
} else { } else {
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping bootstrap publish on subsequent run"); DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
if (nostr_handler_load_system_context_from_adopted_skills() != 0) { if (nostr_handler_load_system_context_from_adopted_skills() != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)"); DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)");
} }
} }
if (persist_runtime_config_to_nostr(&cfg) != 0) {
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
} else {
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
}
startup_step_ok(7, "Reconcile/persist startup state", NULL); startup_step_ok(7, "Reconcile/persist startup state", NULL);
const char* system_context = nostr_handler_get_system_context(); const char* system_context = nostr_handler_get_system_context();

View File

@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0 #define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 79 #define DIDACTYL_VERSION_PATCH 80
#define DIDACTYL_VERSION "v0.0.79" #define DIDACTYL_VERSION "v0.0.80"
// Agent metadata // Agent metadata
#define DIDACTYL_NAME "Didactyl" #define DIDACTYL_NAME "Didactyl"

View File

@@ -1056,6 +1056,167 @@ static int recover_full_config_from_nostr(didactyl_config_t* cfg,
return 0; 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,
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;
int 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) {
if (!cfg) return -1;
cJSON* llm = cJSON_CreateObject();
if (!llm) return -1;
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
char* llm_json = cJSON_PrintUnformatted(llm);
cJSON_Delete(llm);
if (!llm_json) return -1;
int llm_rc = publish_encrypted_self_config_wizard(cfg, "llm_config", llm_json);
free(llm_json);
cJSON* agent = cJSON_CreateObject();
if (!agent) return llm_rc;
cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol));
char* agent_json = cJSON_PrintUnformatted(agent);
cJSON_Delete(agent);
if (!agent_json) return llm_rc;
int agent_rc = publish_encrypted_self_config_wizard(cfg, "agent_config", agent_json);
free(agent_json);
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
}
static int persist_runtime_config_to_nostr_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);
int rc = persist_runtime_config_to_nostr_wizard(cfg);
/* 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) { static int wizard_llm_test(const llm_config_t* llm_cfg) {
if (!llm_cfg) return -1; if (!llm_cfg) return -1;
if (llm_init(llm_cfg) != 0) { if (llm_init(llm_cfg) != 0) {
@@ -2022,6 +2183,13 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
if (c == 's') return 2; if (c == 's') return 2;
if (c == 'b') { 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. */
if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n",
ANSI_YELLOW, ANSI_RESET);
}
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n", fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
ANSI_YELLOW, ANSI_YELLOW,
ANSI_RESET); ANSI_RESET);
@@ -2029,6 +2197,16 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
} }
if (c == 'i') { if (c == 'i') {
/* 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. */
if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
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 (install_system_service_with_dedicated_user(cfg, agent_name) != 0) { 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); fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
return -1; return -1;
@@ -2069,7 +2247,7 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
char nsec_in[OW_MAX_KEY_LEN] = {0}; char nsec_in[OW_MAX_KEY_LEN] = {0};
char agent_name[OW_MAX_NAME_LEN] = {0}; char agent_name[OW_MAX_NAME_LEN] = {0};
int agent_name_found = 0; int agent_name_found = 0;
int config_changed = 0; int relay_changed = 0;
render_wizard_page_header("Existing Agent", "Identity"); render_wizard_page_header("Existing Agent", "Identity");
if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1; if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1;
@@ -2124,6 +2302,7 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
} }
render_wizard_page_header("Existing Agent", "Recovered Configuration"); 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, " Name: %s\n", agent_name);
fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); 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, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)");
@@ -2154,7 +2333,6 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
"Existing Agent -- Administrator") != 0) { "Existing Agent -- Administrator") != 0) {
return -1; return -1;
} }
config_changed = 1;
continue; continue;
} }
if (c == 'l') { if (c == 'l') {
@@ -2163,7 +2341,6 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
"Existing Agent -- LLM Provider") != 0) { "Existing Agent -- LLM Provider") != 0) {
return -1; return -1;
} }
config_changed = 1;
continue; continue;
} }
if (c == 'r') { if (c == 'r') {
@@ -2173,7 +2350,7 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
"Existing Agent -- Relay Configuration"); "Existing Agent -- Relay Configuration");
if (rc < 0) return -1; if (rc < 0) return -1;
if (rc == 1) break; if (rc == 1) break;
config_changed = 1; relay_changed = 1;
break; break;
} }
continue; continue;
@@ -2196,10 +2373,41 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
if (lc == 'q') return -1; if (lc == 'q') return -1;
if (lc == 'b') { if (lc == 'b') {
return config_changed ? SETUP_WIZARD_RC_BOOTSTRAP : SETUP_WIZARD_RC_EXISTING; 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);
return -1;
}
}
/* Always return BOOTSTRAP so main() re-publishes kind 30078.
The wizard has the authoritative config in memory. */
return SETUP_WIZARD_RC_BOOTSTRAP;
} }
if (lc == 'i') { if (lc == 'i') {
/* 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) != 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);
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);
return -1;
}
}
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) { 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); fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
return -1; return -1;