Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7675109afc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,7 +10,7 @@ test_keys.txt
|
||||
|
||||
/mongoose/
|
||||
/Trash/
|
||||
|
||||
deploy_lt.sh
|
||||
|
||||
# Build artifacts
|
||||
/build/
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Current Status — v0.0.78
|
||||
## Current Status — v0.0.79
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.78 — Seed default skill into self-skill cache during startup reconcile
|
||||
> Last release update: v0.0.79 — Enhance existing-agent wizard with full config recovery review edit flow and new-agent fallback
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
@@ -17,6 +17,14 @@ if [ ! -f "$SOURCE_BINARY" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building static binary first..."
|
||||
"$SCRIPT_DIR/build_static.sh"
|
||||
|
||||
if [ ! -f "$SOURCE_BINARY" ]; then
|
||||
echo "ERROR: Build completed but source binary not found: $SOURCE_BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET"
|
||||
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET"
|
||||
echo "Deployment complete."
|
||||
149
plans/enhanced_existing_agent_wizard.md
Normal file
149
plans/enhanced_existing_agent_wizard.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Enhanced Existing Agent Wizard Flow
|
||||
|
||||
## Problem
|
||||
|
||||
When choosing "existing agent" in the wizard, the current flow:
|
||||
1. Asks for nsec
|
||||
2. Recovers kind 10002 relay list from Nostr
|
||||
3. Immediately boots the agent
|
||||
|
||||
This is insufficient when installing an existing agent on a **new server** because:
|
||||
- You cannot review or change the admin pubkey
|
||||
- You cannot review or change the LLM provider/model/API key
|
||||
- You cannot install a systemd service with a dedicated user
|
||||
- You cannot see what config was recovered from Nostr
|
||||
- The agent name is not recovered from the kind 0 profile
|
||||
|
||||
## Current Code
|
||||
|
||||
- [`existing_agent_flow()`](src/setup_wizard.c:1795) — 22 lines, minimal recovery
|
||||
- [`recover_existing_config_from_nostr()`](src/setup_wizard.c:792) — only recovers kind 10002 relays
|
||||
- Returns `SETUP_WIZARD_RC_EXISTING` (2) which does NOT set `bootstrap_mode`
|
||||
- In `main.c`, existing agents skip `reconcile_startup_events()` unless `first_run` is detected
|
||||
|
||||
## Data Available on Nostr for an Existing Agent
|
||||
|
||||
| Data | Kind | Storage | Recovery Method |
|
||||
|------|------|---------|-----------------|
|
||||
| Relay list | 10002 | Public tags | `query_and_extract_kind10002_relays()` |
|
||||
| Agent profile/name | 0 | Public JSON content | Query kind 0 by agent pubkey |
|
||||
| LLM config | 30078 d=llm_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` |
|
||||
| Agent config - admin pubkey, DM protocol | 30078 d=agent_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` |
|
||||
| Default skill | 31124 d=didactyl-default | Public content | Query kind 31124 by agent pubkey |
|
||||
| Adoption list | 10123 | Public tags | Query kind 10123 by agent pubkey |
|
||||
|
||||
## Proposed Enhanced Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Enter nsec] --> B[Connect to default relays]
|
||||
B --> C[Recover kind 10002 relay list]
|
||||
C --> D{Relay list found?}
|
||||
D -->|No| E{Offer to create new agent with this nsec}
|
||||
E -->|Yes| E2[Jump to new_agent_flow with nsec pre-loaded]
|
||||
E -->|No| E3[Return to main menu]
|
||||
D -->|Yes| F[Reconnect with recovered relays]
|
||||
F --> G[Recover all config from Nostr]
|
||||
G --> H[Display recovered config summary]
|
||||
H --> I{Review each setting}
|
||||
I -->|Keep all| J[Review summary + launch options]
|
||||
I -->|Change admin| K[Prompt new admin pubkey]
|
||||
I -->|Change LLM| L[Prompt LLM config]
|
||||
I -->|Change relays| M[Prompt relay config]
|
||||
K --> I
|
||||
L --> I
|
||||
M --> I
|
||||
J --> N{Launch option}
|
||||
N -->|Boot now| O[Return EXISTING]
|
||||
N -->|Install systemd| P[Install dedicated-user service]
|
||||
N -->|Quit| Q[Exit]
|
||||
```
|
||||
|
||||
### Step-by-step
|
||||
|
||||
#### Step 1: Identity — Enter nsec
|
||||
Same as current. Derive keys from nsec.
|
||||
|
||||
#### Step 2: Recovery — Connect and fetch config from Nostr
|
||||
1. Init nostr handler with default relays
|
||||
2. Wait for relay connections
|
||||
3. Query kind 10002 for relay list — if not found, offer to create a new agent with this nsec (jump to `new_agent_flow` with keys pre-loaded, skipping identity step)
|
||||
4. Cleanup and re-init with recovered relays
|
||||
5. Wait for relay connections on recovered relays
|
||||
6. Query kind 0 for agent profile — extract display_name/name
|
||||
7. Query kind 30078 d=llm_config — decrypt and parse LLM settings
|
||||
8. Query kind 30078 d=agent_config — decrypt and parse admin pubkey + DM protocol
|
||||
9. Cleanup nostr handler
|
||||
|
||||
#### Step 3: Review — Present recovered config
|
||||
Display all recovered values:
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Existing Agent -- Recovered Configuration │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Agent name: Simon │
|
||||
│ Identity: b27072b7fc2edf45... │
|
||||
│ Admin: a1b2c3d4e5f6... │
|
||||
│ LLM Provider: ppq │
|
||||
│ LLM Model: claude-haiku-4.5 │
|
||||
│ LLM Base URL: https://api.ppq.ai │
|
||||
│ LLM API Key: sk-...**** │
|
||||
│ DM Protocol: nip04 │
|
||||
│ Relays: 5 configured │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Then offer a menu:
|
||||
```
|
||||
[a] change Admin pubkey
|
||||
[l] change LLM provider/model/key
|
||||
[r] change Relay configuration
|
||||
[c] continue with these settings
|
||||
[q] quit
|
||||
```
|
||||
|
||||
Each change option reuses the existing prompt functions (`prompt_admin_pubkey`, `prompt_llm_config`, `prompt_relay_configuration`) but with context-appropriate headers.
|
||||
|
||||
After any change, redisplay the summary and menu.
|
||||
|
||||
#### Step 4: Launch — Boot or install systemd
|
||||
Same as the new-agent flow's final step:
|
||||
```
|
||||
[b] boot the agent now
|
||||
[i] install dedicated-user systemd service and boot
|
||||
[q] quit
|
||||
```
|
||||
|
||||
The systemd install reuses `install_system_service_with_dedicated_user()`.
|
||||
|
||||
### Return Codes
|
||||
|
||||
- If user chooses "boot now": return `SETUP_WIZARD_RC_EXISTING` (2) — same as current
|
||||
- If user chooses "install systemd": return `SETUP_WIZARD_RC_EXIT` (1) — agent runs as systemd service
|
||||
- If user changed config values: the `main()` flow should still work because `recover_missing_runtime_config_from_nostr()` at line 1108 will fill in any gaps, and the existing agent path at line 1163 loads system context from adopted skills
|
||||
|
||||
### Key Consideration: bootstrap_mode for changed configs
|
||||
|
||||
If the user changes LLM or admin config in the wizard, those changes need to be persisted back to Nostr. Currently, `persist_runtime_config_to_nostr()` is only called when `bootstrap_mode || first_run`.
|
||||
|
||||
**Solution**: When the existing-agent wizard detects that config was changed, return `SETUP_WIZARD_RC_BOOTSTRAP` (0) instead of `SETUP_WIZARD_RC_EXISTING` (2). This triggers the full reconcile path which persists the updated config.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. **`src/setup_wizard.c`**:
|
||||
- Add `recover_full_config_from_nostr()` — fetches kind 0, kind 30078 llm_config, kind 30078 agent_config
|
||||
- Add `query_self_kind0_name()` — queries agent's own kind 0 profile for name
|
||||
- Add `fetch_and_decrypt_self_config()` — replicates `fetch_self_config_plaintext()` logic from main.c for use in wizard context
|
||||
- Rewrite `existing_agent_flow()` with the enhanced multi-step flow
|
||||
- Make `prompt_admin_pubkey()`, `prompt_llm_config()`, `prompt_relay_configuration()` accept a context string parameter for the page header, or add wrapper versions for the existing-agent context
|
||||
|
||||
2. **`src/main.c`**:
|
||||
- Potentially expose `fetch_self_config_plaintext()`, `apply_recalled_llm_config()`, `apply_recalled_agent_config()` as non-static, OR duplicate the logic in setup_wizard.c
|
||||
- Better approach: move these to a shared location or make them accessible via a header
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The wizard already calls `nostr_handler_init()` / `nostr_handler_cleanup()` for validation queries. The enhanced flow will do the same but with two init/cleanup cycles: first with default relays to get kind 10002, then with recovered relays to get everything else.
|
||||
- The `prompt_admin_pubkey()` and `prompt_llm_config()` functions currently have hardcoded step headers like "Step 3 of 7". These should be parameterized or have existing-agent variants.
|
||||
- The `fetch_self_config_plaintext()` function in main.c requires an active nostr handler. The wizard will need to have the handler initialized when calling it.
|
||||
- API key display should be masked — show only first 4 and last 4 characters.
|
||||
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 0
|
||||
#define DIDACTYL_VERSION_PATCH 78
|
||||
#define DIDACTYL_VERSION "v0.0.78"
|
||||
#define DIDACTYL_VERSION_PATCH 79
|
||||
#define DIDACTYL_VERSION "v0.0.79"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -801,6 +801,261 @@ static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_r
|
||||
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, 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;
|
||||
}
|
||||
|
||||
int 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_llm_config_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* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_agent_config_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* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recover_full_config_from_nostr(didactyl_config_t* cfg,
|
||||
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;
|
||||
}
|
||||
|
||||
(void)wait_connected_relays_ms(5000);
|
||||
|
||||
(void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found);
|
||||
|
||||
char* llm_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
|
||||
(void)apply_recalled_llm_config_wizard(cfg, llm_plaintext);
|
||||
}
|
||||
free(llm_plaintext);
|
||||
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
(void)apply_recalled_agent_config_wizard(cfg, agent_plaintext);
|
||||
}
|
||||
free(agent_plaintext);
|
||||
|
||||
nostr_handler_cleanup();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wizard_llm_test(const llm_config_t* llm_cfg) {
|
||||
if (!llm_cfg) return -1;
|
||||
if (llm_init(llm_cfg) != 0) {
|
||||
@@ -1037,10 +1292,12 @@ static int prompt_model_selection(const cJSON* models,
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
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 3 of 7", "New Agent Setup -- Administrator");
|
||||
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;
|
||||
}
|
||||
@@ -1056,9 +1313,15 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
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 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
print_option('1', " ppq.ai");
|
||||
print_option('2', " Routstr");
|
||||
print_option('3', " OpenAI-compatible");
|
||||
@@ -1179,11 +1442,17 @@ static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
}
|
||||
|
||||
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 6 of 7", "New Agent Setup -- Relay Configuration");
|
||||
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] : "");
|
||||
@@ -1235,6 +1504,10 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1794,6 +2067,10 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
|
||||
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 config_changed = 0;
|
||||
|
||||
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 (line_is_quit(nsec_in)) return -1;
|
||||
@@ -1807,13 +2084,153 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
|
||||
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') {
|
||||
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;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
render_wizard_page_header("Existing Agent", "Config Recovery");
|
||||
fprintf(stderr, " Relay list (kind 10002): FOUND (%d relays)\n\n", cfg->relay_count);
|
||||
if (recover_full_config_from_nostr(cfg, 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);
|
||||
}
|
||||
|
||||
return 0;
|
||||
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, " 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;
|
||||
}
|
||||
config_changed = 1;
|
||||
continue;
|
||||
}
|
||||
if (c == 'l') {
|
||||
if (prompt_llm_config_with_header(cfg,
|
||||
"Existing Agent -- Review",
|
||||
"Existing Agent -- LLM Provider") != 0) {
|
||||
return -1;
|
||||
}
|
||||
config_changed = 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;
|
||||
config_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') {
|
||||
return config_changed ? SETUP_WIZARD_RC_BOOTSTRAP : SETUP_WIZARD_RC_EXISTING;
|
||||
}
|
||||
|
||||
if (lc == 'i') {
|
||||
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,
|
||||
"%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);
|
||||
}
|
||||
|
||||
return SETUP_WIZARD_RC_EXIT;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int load_genesis_path_flow(didactyl_config_t* cfg) {
|
||||
@@ -1876,8 +2293,10 @@ int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t g
|
||||
|
||||
if (c == 'e') {
|
||||
int rc = existing_agent_flow(config);
|
||||
if (rc == 0) {
|
||||
return SETUP_WIZARD_RC_EXISTING;
|
||||
if (rc == SETUP_WIZARD_RC_BOOTSTRAP ||
|
||||
rc == SETUP_WIZARD_RC_EXISTING ||
|
||||
rc == SETUP_WIZARD_RC_EXIT) {
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user