feat(setup): quick onboarding, preserve .env vars, clean up boot logging (#821)

* feat(setup): quick onboarding, preserve .env vars, clean up boot logging (#751, #674)

- Add upsert_bootstrap_vars() to preserve user-added .env vars on re-onboarding
- Add --quick mode: auto-defaults DB + security, asks only LLM provider (2 steps)
- Auto-triggered onboarding uses quick mode for near-instant first run
- Fix NEAR AI model fetch to use cloud-api.near.ai when API key is set
- Handle missing WASM tools/channels directories gracefully
- Downgrade all boot/shutdown tracing::info! to debug (boot screen shows user output)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(setup): gate env_backend variable behind postgres feature flag [skip-regression-check]

Clippy lint fix — not a behavioral change, just moving a variable declaration
inside the cfg(feature = "postgres") block where it's used.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(review): address PR review comments

- WASM loaders: use tokio::fs::metadata, only treat NotFound as empty,
  propagate other IO errors, handle TOCTOU in read_dir
- bootstrap: only ignore NotFound in read_to_string, propagate other errors
- wizard: restore print_info/print_success for migrations in interactive
  mode (gated by !config.quick), keep tracing::debug for diagnostics
- tests: use shared crate::config::helpers::ENV_MUTEX instead of separate
  NEARAI_ENV_MUTEX to prevent cross-test env var races
- README: fix quick mode description to mention model selection, clarify
  auto_setup_database may prompt when DATABASE_URL is set

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(setup): skip prompts for DATABASE_URL in quick mode [skip-regression-check]

auto_setup_database() now uses DATABASE_URL directly without calling
step_database_postgres() (which prompts for confirmation). Quick mode
should be fully non-interactive when env vars are already set.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(cli): update --quick help text to mention model selection [skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-03-10 05:02:33 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 94d101924e
commit 3a2989d009
18 changed files with 564 additions and 102 deletions
+14 -14
View File
@@ -241,7 +241,7 @@ impl ToolRegistry {
}
self.register_sync(Arc::new(http));
tracing::info!("Registered {} built-in tools", self.count());
tracing::debug!("Registered {} built-in tools", self.count());
}
/// Register only orchestrator-domain tools (safe for the main process).
@@ -289,7 +289,7 @@ impl ToolRegistry {
self.register_sync(Arc::new(ListDirTool::new()));
self.register_sync(Arc::new(ApplyPatchTool::new()));
tracing::info!("Registered 5 development tools");
tracing::debug!("Registered 5 development tools");
}
/// Register memory tools with a workspace.
@@ -302,7 +302,7 @@ impl ToolRegistry {
self.register_sync(Arc::new(MemoryReadTool::new(Arc::clone(&workspace))));
self.register_sync(Arc::new(MemoryTreeTool::new(workspace)));
tracing::info!("Registered 4 memory tools");
tracing::debug!("Registered 4 memory tools");
}
/// Register job management tools.
@@ -364,7 +364,7 @@ impl ToolRegistry {
job_tool_count += 1;
}
tracing::info!("Registered {} job management tools", job_tool_count);
tracing::debug!("Registered {} job management tools", job_tool_count);
}
/// Register secret management tools (list, delete).
@@ -378,7 +378,7 @@ impl ToolRegistry {
use crate::tools::builtin::{SecretDeleteTool, SecretListTool};
self.register_sync(Arc::new(SecretListTool::new(Arc::clone(&store))));
self.register_sync(Arc::new(SecretDeleteTool::new(store)));
tracing::info!("Registered 2 secret management tools (list, delete)");
tracing::debug!("Registered 2 secret management tools (list, delete)");
}
/// Register extension management tools (search, install, auth, activate, list, remove).
@@ -393,7 +393,7 @@ impl ToolRegistry {
self.register_sync(Arc::new(ToolRemoveTool::new(Arc::clone(&manager))));
self.register_sync(Arc::new(ToolUpgradeTool::new(Arc::clone(&manager))));
self.register_sync(Arc::new(ExtensionInfoTool::new(manager)));
tracing::info!("Registered 8 extension management tools");
tracing::debug!("Registered 8 extension management tools");
}
/// Register skill management tools (list, search, install, remove).
@@ -414,7 +414,7 @@ impl ToolRegistry {
Arc::clone(&catalog),
)));
self.register_sync(Arc::new(SkillRemoveTool::new(registry)));
tracing::info!("Registered 4 skill management tools");
tracing::debug!("Registered 4 skill management tools");
}
/// Register routine management tools.
@@ -448,7 +448,7 @@ impl ToolRegistry {
Arc::clone(&engine),
)));
self.register_sync(Arc::new(RoutineHistoryTool::new(store)));
tracing::info!("Registered 6 routine management tools");
tracing::debug!("Registered 6 routine management tools");
}
/// Register message tool for sending messages to channels.
@@ -467,7 +467,7 @@ impl ToolRegistry {
.write()
.await
.insert("message".to_string());
tracing::info!("Registered message tool");
tracing::debug!("Registered message tool");
}
/// Set the default channel and target for the message tool.
@@ -501,7 +501,7 @@ impl ToolRegistry {
gen_model,
base_dir,
)));
tracing::info!("Registered 2 image tools (generate, edit)");
tracing::debug!("Registered 2 image tools (generate, edit)");
}
/// Register vision/image analysis tools.
@@ -521,7 +521,7 @@ impl ToolRegistry {
vision_model,
base_dir,
)));
tracing::info!("Registered 1 vision tool (analyze)");
tracing::debug!("Registered 1 vision tool (analyze)");
}
/// Register the software builder tool.
@@ -549,7 +549,7 @@ impl ToolRegistry {
self.register(Arc::new(BuildSoftwareTool::new(builder)))
.await;
tracing::info!("Registered software builder tool");
tracing::debug!("Registered software builder tool");
}
/// Register a WASM tool from bytes.
@@ -619,7 +619,7 @@ impl ToolRegistry {
);
}
tracing::info!(name = reg.name, "Registered WASM tool");
tracing::debug!(name = reg.name, "Registered WASM tool");
Ok(())
}
@@ -676,7 +676,7 @@ impl ToolRegistry {
.await
.map_err(WasmRegistrationError::Wasm)?;
tracing::info!(
tracing::debug!(
name = tool_with_binary.tool.name,
user_id = user_id,
trust_level = %tool_with_binary.tool.trust_level,
+36 -8
View File
@@ -193,18 +193,31 @@ impl WasmToolLoader {
///
/// Tools without a capabilities file get no permissions (default deny).
pub async fn load_from_dir(&self, dir: &Path) -> Result<LoadResults, WasmLoadError> {
if !dir.is_dir() {
return Err(WasmLoadError::Io(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
format!("{} is not a directory", dir.display()),
)));
match fs::metadata(dir).await {
Ok(meta) if meta.is_dir() => {}
Ok(_) => {
return Err(WasmLoadError::Io(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
format!("{} is not a directory", dir.display()),
)));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(LoadResults::default());
}
Err(e) => return Err(WasmLoadError::Io(e)),
}
let mut results = LoadResults::default();
// Handle TOCTOU: if read_dir fails with NotFound, treat as empty
let mut entries = match fs::read_dir(dir).await {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(LoadResults::default());
}
Err(e) => return Err(WasmLoadError::Io(e)),
};
// Collect all .wasm entries first, then load in parallel
let mut results = LoadResults::default();
let mut tool_entries = Vec::new();
let mut entries = fs::read_dir(dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
@@ -1077,4 +1090,19 @@ mod tests {
"nested.wasm inside subdir should NOT be discovered"
);
}
#[tokio::test]
async fn load_from_dir_returns_empty_when_dir_missing() {
let loader = make_loader();
let dir = TempDir::new().unwrap();
let missing = dir.path().join("nonexistent_tools_dir");
let results = loader.load_from_dir(&missing).await;
// Must succeed with empty results, not error
let results = results.expect("missing dir should return Ok, not Err");
assert!(results.loaded.is_empty());
assert!(results.errors.is_empty());
}
}