From 5311d23527a6b45f094ea46329e343ef469bfaa3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Mar 2026 07:07:30 -0400 Subject: [PATCH] v0.0.76 - Use sudo-prefixed privileged commands in setup wizard install flow with password prompting and remove root-shell handoff --- README.md | 4 +- src/main.h | 4 +- src/setup_wizard.c | 280 +++++++++++++++++++++++++-------------------- 3 files changed, 157 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index ba7de0c..73d7176 100644 --- a/README.md +++ b/README.md @@ -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.75 +## Current Status — v0.0.76 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.0.75 — Add rsync deploy script and wizard root-shell handoff for service install path +> Last release update: v0.0.76 — Use sudo-prefixed privileged commands in setup wizard install flow with password prompting and remove root-shell handoff - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/main.h b/src/main.h index e474b67..91d8754 100644 --- a/src/main.h +++ b/src/main.h @@ -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 75 -#define DIDACTYL_VERSION "v0.0.75" +#define DIDACTYL_VERSION_PATCH 76 +#define DIDACTYL_VERSION "v0.0.76" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 441c96a..2766c14 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -1235,12 +1235,6 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) { } } -static int ensure_dir_exists_local(const char* path) { - if (!path || path[0] == '\0') return -1; - if (mkdir(path, 0755) == 0 || errno == EEXIST) return 0; - return -1; -} - static int run_command_local(char* const argv[]) { pid_t pid = fork(); if (pid < 0) return -1; @@ -1255,31 +1249,64 @@ static int run_command_local(char* const argv[]) { return 0; } -static int copy_file_local(const char* src, const char* dst) { - if (!src || !dst || src[0] == '\0' || dst[0] == '\0') return -1; +static int run_privileged_command_local(char* const argv[]) { + if (!argv || !argv[0]) return -1; - FILE* in = fopen(src, "rb"); - if (!in) return -1; + if (geteuid() == 0) { + return run_command_local(argv); + } - FILE* out = fopen(dst, "wb"); - if (!out) { - fclose(in); + 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; } - char buf[16384]; - size_t n = 0; - while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { - if (fwrite(buf, 1, n, out) != n) { - fclose(in); - fclose(out); - 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; } - fclose(in); - fclose(out); - return 0; + 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) { @@ -1332,37 +1359,44 @@ static int write_sudoers_file(const char* service_user) { char sudoers_path[PATH_MAX] = {0}; snprintf(sudoers_path, sizeof(sudoers_path), "/etc/sudoers.d/%s", service_user); - FILE* fp = fopen(sudoers_path, "w"); - if (!fp) { - fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET); + 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; } - fprintf(fp, - "# 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); - fclose(fp); - - if (chmod(sudoers_path, 0440) != 0) { - fprintf(stderr, "%sFailed to chmod sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET); + 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; } @@ -1373,25 +1407,12 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c if (!cfg || cfg->keys.nsec[0] == '\0') return -1; if (geteuid() != 0) { - fprintf(stderr, "%sSystem install requires root.%s\n", ANSI_RED, ANSI_RESET); - fprintf(stderr, " You can open a root shell now and then re-run this install step.\n"); - print_option('o', "pen root shell now (sudo -i)"); - print_option('r', "eturn without installing"); - wizard_option_t root_opts[] = {{'o', ""}, {'r', ""}}; - char root_choice = read_menu_choice(root_opts, 2); - - if (root_choice == 'o') { - char* sudo_argv[] = {"sudo", "-i", NULL}; - if (run_command_local(sudo_argv) != 0) { - fprintf(stderr, "%sFailed to open root shell via sudo.%s\n", ANSI_RED, ANSI_RESET); - } else { - fprintf(stderr, "%sReturned from root shell.%s\n", ANSI_YELLOW, ANSI_RESET); - } - fprintf(stderr, - " Re-run didactyl as root and choose install again to complete system service setup.\n"); + 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; } - - return -1; } /* --- Derive service username from agent name --- */ @@ -1405,7 +1426,7 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c 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_command_local(useradd_argv) != 0) { + 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; } @@ -1424,11 +1445,6 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c return -1; } - if (ensure_dir_exists_local(home) != 0) { - fprintf(stderr, "%sUnable to create/access home directory: %s%s\n", ANSI_RED, home, 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); @@ -1441,19 +1457,23 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c char bin_path[PATH_MAX] = {0}; snprintf(bin_path, sizeof(bin_path), "%s/didactyl", home); fprintf(stderr, " Copying binary to %s...\n", bin_path); - if (copy_file_local(self_path, bin_path) != 0) { + 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; } - if (chmod(bin_path, 0755) != 0) { - fprintf(stderr, "%sFailed to chmod binary at %s.%s\n", ANSI_RED, bin_path, ANSI_RESET); - return -1; - } /* --- Set ownership on home directory and installed binary --- */ - if (chown(home, pw->pw_uid, pw->pw_gid) != 0 || - chmod(home, 0750) != 0 || - chown(bin_path, pw->pw_uid, pw->pw_gid) != 0) { + 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; } @@ -1476,60 +1496,66 @@ static int install_system_service_with_dedicated_user(const didactyl_config_t* c 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); - FILE* svc_fp = fopen(service_path, "w"); - if (!svc_fp) { - fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET); + + char env_line[PATH_MAX + 64] = {0}; + if (ca_bundle) { + snprintf(env_line, sizeof(env_line), "Environment=SSL_CERT_FILE=%s\\n", ca_bundle); + } + + 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=simple\n" + "User=%s\n" + "Group=%s\n" + "WorkingDirectory=%s\n" + "ExecStart=%s --nsec %s --debug 3\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, + cfg->keys.nsec, + 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; } - fprintf(svc_fp, - "[Unit]\n" - "Description=Didactyl Nostr Agent (%s)\n" - "After=network-online.target\n" - "Wants=network-online.target\n\n" - "[Service]\n" - "Type=simple\n" - "User=%s\n" - "Group=%s\n" - "WorkingDirectory=%s\n" - "ExecStart=%s --nsec %s --debug 3\n", - service_user, - service_user, - service_user, - home, - bin_path, - cfg->keys.nsec); - - if (ca_bundle) { - fprintf(svc_fp, "Environment=SSL_CERT_FILE=%s\n", ca_bundle); + 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; } - fprintf(svc_fp, - "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, - home); - fclose(svc_fp); - /* --- 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_command_local(reload_argv) != 0 || - run_command_local(enable_argv) != 0 || - run_command_local(start_argv) != 0) { + 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");