Skip to content

fix(setup): remove redundant LLM config and API keys from bootstrap .env - #1448

Merged
ilblackdragon merged 5 commits into
stagingfrom
fix/onboard-env-secrets-auth
Mar 20, 2026
Merged

ilblackdragon merged 5 commits into
stagingfrom
fix/onboard-env-secrets-auth

Conversation

@ilblackdragon

@ilblackdragon ilblackdragon commented Mar 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Supersedes #266.

Investigation revealed that ~/.ironclaw/.env should only contain true chicken-and-egg vars — things needed before the DB connects or to decrypt secrets. Everything else is redundant:

  • LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name, Bedrock config, provider-specific URLs) are persisted to the DB via persist_settings() and loaded by Config::from_db_with_toml() after connection
  • API keys (NEARAI_API_KEY, ANTHROPIC_API_KEY, etc.) are stored encrypted in the secrets DB and injected via inject_llm_keys_from_secrets() — writing them as plaintext to .env was redundant and a security regression
  • Explicit env vars still win — inject_llm_keys_from_secrets() skips injection when std::env::var(env_var) is already set (line 416-417 of config/mod.rs), so users who set NEARAI_API_KEY in their shell profile are unaffected

What stays in bootstrap .env

Var Why
DATABASE_BACKEND Needed to connect to DB
DATABASE_URL / LIBSQL_PATH / LIBSQL_URL Needed to connect to DB
SECRETS_MASTER_KEY Needed to decrypt secrets store
ONBOARD_COMPLETED Checked before DB connection
Channel vars (Signal, etc.) Channel init may precede DB — left for a separate PR

Changes

  • Remove LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, Bedrock vars, model name, provider-specific base URLs, NEARAI_API_KEY from write_bootstrap_env() (-61 lines)
  • Fix NearAiConfig::for_model_discovery() and build_nearai_model_fetch_config() to use env_or_override() instead of std::env::var() — these were reading API keys from the real process env, missing keys set via inject_single_var() during the onboarding wizard. This caused model fetching to fail after entering an API key during setup.
  • Fix incorrect secret names in README (anthropic_api_key → llm_anthropic_api_key, openai_api_key → llm_openai_api_key)
  • Update README bootstrap vars documentation

What about PR #266?

Everything proposed in #266 already landed on staging:

  • Early .env loading in main.rs (before command dispatch) ✅
  • SECRETS_MASTER_KEY persistence via settings.secrets_master_key_hex ✅
  • Thread-safe inject_single_var() instead of unsafe set_var ✅

Test plan

  • cargo clippy --all --benches --tests --examples --all-features — zero warnings
  • cargo test --lib setup — 63 tests pass
  • cargo test --lib build_nearai_model_fetch — 3 tests pass (API key overlay resolution)
  • Manual: run onboard wizard with NEAR AI Cloud, verify model fetching works after entering API key
  • Manual: restart, verify only DB/secrets/onboard vars in ~/.ironclaw/.env
  • Manual: verify LLM config loads from DB and API keys from encrypted secrets store

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 20, 2026 02:57
@github-actions github-actions Bot added scope: setup Onboarding / setup scope: docs Documentation size: S 10-49 changed lines risk: high Safety, secrets, auth, or critical infrastructure labels Mar 20, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the setup process by ensuring that API keys for selected Large Language Model (LLM) providers are persistently stored in the bootstrap .env file. This change guarantees that these critical keys are available immediately upon application startup, even before the main secrets database is fully initialized, thereby improving the robustness and reliability of the system's initial configuration.

Highlights

  • API Key Persistence: Implemented persistence for selected LLM provider API keys (e.g., ANTHROPIC_API_KEY, OPENAI_API_KEY, LLM_API_KEY) to ~/.ironclaw/.env via write_bootstrap_env(). This ensures keys are available on restart before the secrets database connects.
  • Code Pattern Adherence: The implementation utilizes the existing env_or_override() and provider registry patterns, avoiding hardcoded environment variable names or unsafe set_var calls.
  • Documentation Correction: Corrected incorrect secret names in src/setup/README.md, updating anthropic_api_key to llm_anthropic_api_key and openai_api_key to llm_openai_api_key.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request persists LLM provider API keys to the ~/.ironclaw/.env file to ensure they are available on restart before the secrets database is connected. The implementation correctly retrieves the API key from the runtime overlay and adds it to the bootstrap environment variables. The documentation in src/setup/README.md has been updated accordingly to reflect this change and to fix some incorrect secret names. My review found one minor issue related to an outdated doc comment in wizard.rs that should be updated to align with the new behavior.

Comment thread src/setup/wizard.rs Outdated
Comment on lines +2659 to +2670
// Persist the selected provider's API key to bootstrap .env so it's
// available on next startup before the secrets DB is connected.
// Uses the same thread-safe overlay that setup_api_key_provider() writes to.
if let Some(ref backend) = self.settings.llm_backend
&& let Some(def) = registry.find(backend)
&& let Some(ref env_name) = def.api_key_env
&& env_name != "NEARAI_API_KEY"
&& let Some(api_key) = crate::config::helpers::env_or_override(env_name)
&& !api_key.is_empty()
{
env_vars.push((env_name.clone(), api_key));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With this change, the function's doc comment is now outdated. It states that **Credentials are NOT written here.**, but this new logic explicitly persists provider API keys to the .env file. Please update the doc comment for write_bootstrap_env to reflect this new behavior and avoid confusion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — this code block was removed in a subsequent push. We no longer persist any API keys to bootstrap .env.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the setup/onboarding flow to persist the selected LLM provider’s API key into the bootstrap ~/.ironclaw/.env, ensuring the key is still available across restarts before the encrypted secrets DB can be opened. It also corrects provider secret-name documentation in the setup README.

Changes:

  • Persist the selected registry provider’s api_key_env value (e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_API_KEY) into the bootstrap .env during write_bootstrap_env().
  • Fix incorrect secret-name entries for Anthropic/OpenAI in src/setup/README.md.
  • Document bootstrap vars as including SECRETS_MASTER_KEY and the provider API-key env var.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/setup/wizard.rs Adds logic to include the selected provider’s API key in the bootstrap .env written by write_bootstrap_env().
src/setup/README.md Corrects documented secret names for providers and updates the list of bootstrap vars written to ~/.ironclaw/.env.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/setup/wizard.rs Outdated
Comment on lines +2660 to +2666
// available on next startup before the secrets DB is connected.
// Uses the same thread-safe overlay that setup_api_key_provider() writes to.
if let Some(ref backend) = self.settings.llm_backend
&& let Some(def) = registry.find(backend)
&& let Some(ref env_name) = def.api_key_env
&& env_name != "NEARAI_API_KEY"
&& let Some(api_key) = crate::config::helpers::env_or_override(env_name)

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this reads from the “thread-safe overlay”, but env_or_override() checks real env vars first and only then falls back to runtime overrides / injected vars. Consider rewording to avoid implying it only persists keys set via the overlay.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — this code block was removed. We no longer persist provider API keys to bootstrap .env at all.

Comment thread src/setup/wizard.rs Outdated
Comment on lines +2662 to +2670
if let Some(ref backend) = self.settings.llm_backend
&& let Some(def) = registry.find(backend)
&& let Some(ref env_name) = def.api_key_env
&& env_name != "NEARAI_API_KEY"
&& let Some(api_key) = crate::config::helpers::env_or_override(env_name)
&& !api_key.is_empty()
{
env_vars.push((env_name.clone(), api_key));
}

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces new behavior (persisting the selected provider’s API key to the bootstrap .env) but there doesn’t appear to be test coverage for write_bootstrap_env(). Please add a unit/integration test that sets llm_backend to a registry provider with api_key_env, injects the key via the overlay, runs write_bootstrap_env(), and asserts the key is present in the resulting ~/.ironclaw/.env (ideally using a temp base dir or a testable path variant to avoid touching the real home directory).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — the API key persistence code was removed entirely. The test_model_discovery_picks_up_injected_var test was added for the env_or_override() fix in for_model_discovery().

Comment thread src/setup/README.md Outdated
- `OLLAMA_BASE_URL` (if ollama)
- `NEARAI_API_KEY` (if API key auth path)
- `SECRETS_MASTER_KEY` (if env key source selected in Step 2)
- Provider API key env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `LLM_API_KEY`) — persisted from the thread-safe overlay when the selected provider has an `api_key_env`

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullet says the provider API key env var is “persisted from the thread-safe overlay”, but the implementation uses env_or_override(), which also reads real env vars first. Please adjust this wording to match actual behavior (e.g., “persisted from env_or_override() / current env scope”) so operators understand when a key will be written.

Suggested change
- Provider API key env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `LLM_API_KEY`) — persisted from the thread-safe overlay when the selected provider has an `api_key_env`
- Provider API key env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `LLM_API_KEY`) — persisted from `env_or_override()` (current env scope) when the selected provider has an `api_key_env`

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — this README line was removed along with the API key persistence code.

Comment thread src/setup/wizard.rs Outdated
Comment on lines +2659 to +2661
// Persist the selected provider's API key to bootstrap .env so it's
// available on next startup before the secrets DB is connected.
// Uses the same thread-safe overlay that setup_api_key_provider() writes to.

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_bootstrap_env()’s doc comment says credentials are not written here, but this new block persists the selected provider’s API key to ~/.ironclaw/.env. Please update the function-level documentation to reflect the new behavior (and clarify which credentials may be written) so future readers aren’t misled.

Suggested change
// Persist the selected provider's API key to bootstrap .env so it's
// available on next startup before the secrets DB is connected.
// Uses the same thread-safe overlay that setup_api_key_provider() writes to.
// Persist the selected non-NEAR AI provider's API key to the bootstrap
// .env so it's available on next startup before the secrets DB is
// connected. The API key value is read from the same thread-safe runtime
// env overlay that setup_api_key_provider() writes to, and then written
// out as a regular environment variable for that provider.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — this code block was removed. No provider API keys are written to bootstrap .env anymore.

@ilblackdragon
ilblackdragon force-pushed the fix/onboard-env-secrets-auth branch from 83247bc to fbb4d83 Compare March 20, 2026 03:26
@github-actions github-actions Bot added size: XS < 10 changed lines (excluding docs) and removed size: S 10-49 changed lines labels Mar 20, 2026
@ilblackdragon ilblackdragon changed the title fix(setup): persist provider API keys to bootstrap .env fix(setup): remove plaintext API keys from bootstrap .env Mar 20, 2026
Copilot AI review requested due to automatic review settings March 20, 2026 03:31
@ilblackdragon
ilblackdragon force-pushed the fix/onboard-env-secrets-auth branch from fbb4d83 to 8b474c0 Compare March 20, 2026 03:31
@github-actions github-actions Bot added size: M 50-199 changed lines and removed size: XS < 10 changed lines (excluding docs) labels Mar 20, 2026
@ilblackdragon ilblackdragon changed the title fix(setup): remove plaintext API keys from bootstrap .env fix(setup): remove redundant LLM config and API keys from bootstrap .env Mar 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/setup/wizard.rs Outdated
Comment on lines 2580 to 2585
/// re-resolution in `AppBuilder::build_all()` fills them in after
/// `inject_llm_keys_from_secrets()` loads from encrypted storage.
fn write_bootstrap_env(&self) -> Result<(), SetupError> {
let registry = crate::llm::ProviderRegistry::load();
let mut env_vars: Vec<(String, String)> = Vec::new();

if let Some(ref backend) = self.settings.database_backend {

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_bootstrap_env()'s rustdoc (just above) describes persisting LLM bootstrap settings like LLM_BACKEND, but the function now only writes DB/secrets/onboarding (+ some channel/sandbox) vars. Please align the documentation with the current behavior (or re-add any non-secret LLM bootstrap vars that are still required before DB init).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the write_bootstrap_env() docstring to accurately list only DB/secrets/onboard/channel vars. LLM settings are explicitly called out as NOT written here.

Comment thread src/setup/wizard.rs
Comment on lines 2595 to 2600
env_vars.push(("LIBSQL_URL".to_string(), url.clone()));
}

// LLM bootstrap vars: same chicken-and-egg problem as DATABASE_BACKEND.
// Config::from_env() needs the backend before the DB is connected.
if let Some(ref backend) = self.settings.llm_backend {
env_vars.push(("LLM_BACKEND".to_string(), backend.clone()));
}
if let Some(ref url) = self.settings.openai_compatible_base_url {
env_vars.push(("LLM_BASE_URL".to_string(), url.clone()));
}
if let Some(ref url) = self.settings.ollama_base_url {
env_vars.push(("OLLAMA_BASE_URL".to_string(), url.clone()));
}
if let Some(ref region) = self.settings.bedrock_region {
env_vars.push(("BEDROCK_REGION".to_string(), region.clone()));
}
if self.settings.llm_backend.as_deref() == Some("bedrock") {
if let Some(ref model) = self.settings.selected_model {
env_vars.push(("BEDROCK_MODEL".to_string(), model.clone()));
}
if let Some(ref cross) = self.settings.bedrock_cross_region {
env_vars.push(("BEDROCK_CROSS_REGION".to_string(), cross.clone()));
}
if let Some(ref profile) = self.settings.bedrock_profile {
env_vars.push(("AWS_PROFILE".to_string(), profile.clone()));
}
}

// Model name: same chicken-and-egg — Config::from_env() resolves the
// model before the DB is connected, so we must persist it to .env.
// Write the backend-specific env var so the correct resolution path
// picks it up (looked up from the provider registry).
// Bedrock model is already written above as BEDROCK_MODEL, skip here.
if self.settings.llm_backend.as_deref() != Some("bedrock")
&& let Some(ref model) = self.settings.selected_model
{
let backend_str = self.settings.llm_backend.as_deref().unwrap_or("nearai");
let model_env = registry.model_env_var(backend_str);
env_vars.push((model_env.to_string(), model.clone()));
}

// Also write provider-specific base URL env var if the provider
// defines one (e.g., GROQ doesn't need LLM_BASE_URL since its
// default is compiled in, but it doesn't hurt to be explicit).
if let Some(ref backend) = self.settings.llm_backend
&& let Some(def) = registry.find(backend)
&& let Some(ref base_url_env) = def.base_url_env
&& let Some(ref base_url) = def.default_base_url
&& base_url_env != "LLM_BASE_URL"
&& base_url_env != "OLLAMA_BASE_URL"
{
env_vars.push((base_url_env.clone(), base_url.clone()));
}

// Preserve NEARAI_API_KEY if present (set by API key auth flow
// via the thread-safe runtime env overlay).
if let Some(api_key) = crate::config::helpers::env_or_override("NEARAI_API_KEY")
&& !api_key.is_empty()
{
env_vars.push(("NEARAI_API_KEY".to_string(), api_key));
}

// Secrets master key (env var mode): write to .env so it's available
// on next startup before the DB is connected.
if let Some(ref key_hex) = self.settings.secrets_master_key_hex {

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes all LLM-related bootstrap persistence from write_bootstrap_env() (backend/base URLs/model), not just plaintext API keys. The PR description calls out LLM_BACKEND as a chicken-and-egg var that should still be written to ~/.ironclaw/.env; please clarify the intended behavior and make the implementation + docs consistent.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — LLM_BACKEND is NOT a chicken-and-egg var. Config::from_db_with_toml() loads it from DB settings after connection, and LlmConfig::resolve() falls back to settings.llm_backend when the env var is absent. Only DB connection params, SECRETS_MASTER_KEY, and ONBOARD_COMPLETED are true chicken-and-egg vars. Updated the docstring and README to clarify.

Comment thread src/setup/README.md
Comment on lines +484 to 491
Bootstrap vars written to `~/.ironclaw/.env` (only true chicken-and-egg vars
that are needed before the DB is connected):
- `DATABASE_BACKEND` (always)
- `DATABASE_URL` (if postgres)
- `LIBSQL_PATH` (if libsql)
- `LIBSQL_URL` (if turso sync)
- `LLM_BACKEND` (always, when set)
- `LLM_BASE_URL` (if openai_compatible)
- `OLLAMA_BASE_URL` (if ollama)
- `NEARAI_API_KEY` (if API key auth path)
- `SECRETS_MASTER_KEY` (if env key source selected in Step 2)
- `ONBOARD_COMPLETED` (always, "true")

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bootstrap var list here appears incomplete compared to what write_bootstrap_env() actually writes (e.g., CLAUDE_CODE_ENABLED and multiple SIGNAL_* vars are also persisted to ~/.ironclaw/.env). Please either document these additional vars or clarify that the list is non-exhaustive.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added CLAUDE_CODE_ENABLED, SIGNAL_HTTP_URL, SIGNAL_ACCOUNT, etc. to the bootstrap vars list with a note that channel init may precede DB.

Only true chicken-and-egg vars belong in ~/.ironclaw/.env — things needed
to connect to the DB or decrypt secrets (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, SECRETS_MASTER_KEY, ONBOARD_COMPLETED).

LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name,
provider-specific URLs) are persisted to the DB via persist_settings()
and loaded by Config::from_db_with_toml() after connection. API keys are
stored encrypted in the secrets DB and injected via
inject_llm_keys_from_secrets(). Writing them as plaintext to .env was
redundant and a security regression.

Also fixes for_model_discovery() and build_nearai_model_fetch_config()
to use env_or_override() instead of std::env::var(), so they can read
NEARAI_API_KEY from the thread-safe overlay during the onboarding wizard
(where inject_single_var() sets the key after the user enters it).

Also fixes incorrect secret names in README (anthropic_api_key →
llm_anthropic_api_key, openai_api_key → llm_openai_api_key).

Supersedes #266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon
ilblackdragon force-pushed the fix/onboard-env-secrets-auth branch from 8b474c0 to 4f183d6 Compare March 20, 2026 04:27
@github-actions github-actions Bot added the scope: llm LLM integration label Mar 20, 2026
Copilot AI review requested due to automatic review settings March 20, 2026 06:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/setup/README.md
Comment on lines +499 to +502
LLM settings (`LLM_BACKEND`, `LLM_BASE_URL`, model, API keys) are persisted
to the DB via `persist_settings()` and loaded by `Config::from_db_with_toml()`
after connection. API keys are stored encrypted in the secrets DB and injected
via `inject_llm_keys_from_secrets()`.

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This updated section says bootstrap .env only includes DB/secrets/onboard vars, but earlier in this README there are still example ~/.ironclaw/.env blocks and explanations that include LLM_BACKEND/LLM_BASE_URL (and imply some LLM config must live in Layer 1). Please update those earlier examples/explanations to avoid contradicting the new bootstrap-var policy.

Suggested change
LLM settings (`LLM_BACKEND`, `LLM_BASE_URL`, model, API keys) are persisted
to the DB via `persist_settings()` and loaded by `Config::from_db_with_toml()`
after connection. API keys are stored encrypted in the secrets DB and injected
via `inject_llm_keys_from_secrets()`.
No LLM-related variables (e.g. `LLM_BACKEND`, `LLM_BASE_URL`, model name, or
API keys) are written to the bootstrap `~/.ironclaw/.env`. LLM settings are
persisted to the DB via `persist_settings()` and loaded by
`Config::from_db_with_toml()` after connection. API keys are stored encrypted
in the secrets DB and injected via `inject_llm_keys_from_secrets()`. Any
earlier example `.env` blocks that show `LLM_*` entries are legacy and should
not be used with the current design.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the Layer 1 .env examples to remove LLM_BACKEND/LLM_BASE_URL/OLLAMA_BASE_URL and show only DB + secrets + onboard vars. Also updated the "Why separate?" explanation.

Comment thread src/setup/README.md
Comment on lines 216 to +220
|----------|-------------|-------------|---------|
| NEAR AI Chat | Browser OAuth or session token | - | `NEARAI_SESSION_TOKEN` |
| NEAR AI Cloud | API key | `llm_nearai_api_key` | `NEARAI_API_KEY` |
| Anthropic | API key | `anthropic_api_key` | `ANTHROPIC_API_KEY` |
| OpenAI | API key | `openai_api_key` | `OPENAI_API_KEY` |
| Anthropic | API key | `llm_anthropic_api_key` | `ANTHROPIC_API_KEY` |
| OpenAI | API key | `llm_openai_api_key` | `OPENAI_API_KEY` |

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provider table now correctly uses secret names like llm_anthropic_api_key/llm_openai_api_key, but later in the README ("Secret Storage") the example still refers to legacy names like anthropic_api_key. Please update that example to use the current secret keys for consistency.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the secret name example from anthropic_api_key to llm_anthropic_api_key.

Comment thread src/setup/wizard.rs Outdated
Comment on lines 2627 to 2629
/// re-resolution in `AppBuilder::build_all()` fills them in after
/// `inject_llm_keys_from_secrets()` loads from encrypted storage.
fn write_bootstrap_env(&self) -> Result<(), SetupError> {

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rustdoc immediately above this function still claims bootstrap vars include LLM_BACKEND (and other LLM-related chicken-and-egg settings), but this function no longer writes any LLM vars. Please update that rustdoc to reflect the new behavior (bootstrap env is DB/secrets/onboarding only).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the rustdoc now accurately states that only DB/secrets/onboard/channel vars are written, and explicitly says LLM settings and credentials are NOT written here.

Only true chicken-and-egg vars belong in ~/.ironclaw/.env — things needed
to connect to the DB or decrypt secrets (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, SECRETS_MASTER_KEY, ONBOARD_COMPLETED).

LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name,
provider-specific URLs) are persisted to the DB via persist_settings()
and loaded by Config::from_db_with_toml() after connection. API keys are
stored encrypted in the secrets DB and injected via
inject_llm_keys_from_secrets(). Writing them as plaintext to .env was
redundant and a security regression.

Also fixes for_model_discovery() and build_nearai_model_fetch_config()
to use env_or_override() instead of std::env::var(), so they can read
NEARAI_API_KEY from the thread-safe overlay during the onboarding wizard
(where inject_single_var() sets the key after the user enters it).

Also fixes incorrect secret names in README (anthropic_api_key →
llm_anthropic_api_key, openai_api_key → llm_openai_api_key).

Supersedes #266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon
ilblackdragon force-pushed the fix/onboard-env-secrets-auth branch from 6a6b78c to 53e1395 Compare March 20, 2026 06:15
Copilot AI review requested due to automatic review settings March 20, 2026 06:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/setup/wizard.rs
Comment on lines 2629 to 2632
fn write_bootstrap_env(&self) -> Result<(), SetupError> {
let registry = crate::llm::ProviderRegistry::load();
let mut env_vars: Vec<(String, String)> = Vec::new();

if let Some(ref backend) = self.settings.database_backend {

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment immediately above write_bootstrap_env() still states bootstrap vars include LLM_BACKEND ("DATABASE_BACKEND, DATABASE_URL, LLM_BACKEND, etc."), but this function no longer writes any LLM-related vars. Please update that comment to match current behavior so readers don’t assume LLM config still comes from ~/.ironclaw/.env before DB connection.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest push — docstring updated.

Comment thread src/setup/wizard.rs
Comment on lines 2642 to 2647
env_vars.push(("LIBSQL_URL".to_string(), url.clone()));
}

// LLM bootstrap vars: same chicken-and-egg problem as DATABASE_BACKEND.
// Config::from_env() needs the backend before the DB is connected.
if let Some(ref backend) = self.settings.llm_backend {
env_vars.push(("LLM_BACKEND".to_string(), backend.clone()));
}
if let Some(ref url) = self.settings.openai_compatible_base_url {
env_vars.push(("LLM_BASE_URL".to_string(), url.clone()));
}
if let Some(ref url) = self.settings.ollama_base_url {
env_vars.push(("OLLAMA_BASE_URL".to_string(), url.clone()));
}
if let Some(ref region) = self.settings.bedrock_region {
env_vars.push(("BEDROCK_REGION".to_string(), region.clone()));
}
if self.settings.llm_backend.as_deref() == Some("bedrock") {
if let Some(ref model) = self.settings.selected_model {
env_vars.push(("BEDROCK_MODEL".to_string(), model.clone()));
}
if let Some(ref cross) = self.settings.bedrock_cross_region {
env_vars.push(("BEDROCK_CROSS_REGION".to_string(), cross.clone()));
}
if let Some(ref profile) = self.settings.bedrock_profile {
env_vars.push(("AWS_PROFILE".to_string(), profile.clone()));
}
}

// Model name: same chicken-and-egg — Config::from_env() resolves the
// model before the DB is connected, so we must persist it to .env.
// Write the backend-specific env var so the correct resolution path
// picks it up (looked up from the provider registry).
// Bedrock model is already written above as BEDROCK_MODEL, skip here.
if self.settings.llm_backend.as_deref() != Some("bedrock")
&& let Some(ref model) = self.settings.selected_model
{
let backend_str = self.settings.llm_backend.as_deref().unwrap_or("nearai");
let model_env = registry.model_env_var(backend_str);
env_vars.push((model_env.to_string(), model.clone()));
}

// Also write provider-specific base URL env var if the provider
// defines one (e.g., GROQ doesn't need LLM_BASE_URL since its
// default is compiled in, but it doesn't hurt to be explicit).
if let Some(ref backend) = self.settings.llm_backend
&& let Some(def) = registry.find(backend)
&& let Some(ref base_url_env) = def.base_url_env
&& let Some(ref base_url) = def.default_base_url
&& base_url_env != "LLM_BASE_URL"
&& base_url_env != "OLLAMA_BASE_URL"
{
env_vars.push((base_url_env.clone(), base_url.clone()));
}

// Preserve NEARAI_API_KEY if present (set by API key auth flow
// via the thread-safe runtime env overlay).
if let Some(api_key) = crate::config::helpers::env_or_override("NEARAI_API_KEY")
&& !api_key.is_empty()
{
env_vars.push(("NEARAI_API_KEY".to_string(), api_key));
}

// Secrets master key (env var mode): write to .env so it's available
// on next startup before the DB is connected.
if let Some(ref key_hex) = self.settings.secrets_master_key_hex {

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because write_bootstrap_env() uses upsert_bootstrap_vars() (which preserves unknown keys in the existing ~/.ironclaw/.env), simply omitting LLM vars from env_vars will NOT remove previously-written entries like NEARAI_API_KEY, LLM_BACKEND, or LLM_BASE_URL from existing installations. This can leave plaintext API keys/config lingering in the bootstrap file even after re-running the wizard. Consider explicitly pruning known-deprecated keys from the file (or writing empty values / switching to a write-mode that rewrites only the allowed bootstrap keys) so the on-disk .env actually matches the “only chicken-and-egg vars” invariant.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point — upsert_bootstrap_vars() preserves unknown keys, so previously-written NEARAI_API_KEY/LLM_BACKEND entries will linger in existing installations. This is low-risk (they become redundant overrides, not incorrect), but a cleanup pass could be added as a follow-up. The lingering NEARAI_API_KEY is the main concern — it's a plaintext API key that should ideally be pruned. Filed as a TODO for a follow-up PR.

Comment thread src/setup/wizard.rs
Comment on lines +3841 to +3845
crate::config::inject_single_var("NEARAI_API_KEY", "injected-wizard-key");
let config = build_nearai_model_fetch_config();

// Clean up
crate::config::inject_single_var("NEARAI_API_KEY", "");

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mutates the global INJECTED_VARS overlay via inject_single_var(), but the cleanup sets the key to an empty string rather than removing/restoring it. Because other config readers (notably optional_env()) don’t consistently treat empty injected values as unset, leaving NEARAI_API_KEY present-but-empty in INJECTED_VARS can leak state across tests and cause order-dependent failures. Please restore the previous injected value or add a test-only helper to remove a key from INJECTED_VARS (or adjust optional_env() to filter out empty injected values) so this test can’t pollute global state.

Suggested change
crate::config::inject_single_var("NEARAI_API_KEY", "injected-wizard-key");
let config = build_nearai_model_fetch_config();
// Clean up
crate::config::inject_single_var("NEARAI_API_KEY", "");
// Capture any previously configured API key (from env or injected overlay)
let previous_injected_api_key = crate::config::optional_env("NEARAI_API_KEY");
crate::config::inject_single_var("NEARAI_API_KEY", "injected-wizard-key");
let config = build_nearai_model_fetch_config();
// Clean up: restore the previous value if there was one, rather than
// forcing an empty string into the injected overlay.
if let Some(prev_key) = previous_injected_api_key {
crate::config::inject_single_var("NEARAI_API_KEY", &prev_key);
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment explaining the cleanup. env_or_override() explicitly filters empty values at every layer (real env line 54, runtime overrides line 64, INJECTED_VARS line 75 of helpers.rs), so setting to empty string is functionally equivalent to removal. No state leaks across tests since the ENV_MUTEX serializes all env-touching tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the scope: agent Agent core (agent loop, router, scheduler) label Mar 20, 2026
@ilblackdragon
ilblackdragon requested a review from zmanian March 20, 2026 07:33
- Update write_bootstrap_env() docstring to reflect current behavior
  (no LLM vars, no credentials)
- Fix Layer 1 .env examples in README to remove LLM_BACKEND/LLM_BASE_URL
- Fix legacy secret name in README example (anthropic_api_key →
  llm_anthropic_api_key)
- Document channel/sandbox vars in bootstrap vars list
- Add cleanup comment in test explaining empty-value-as-unset behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 20, 2026 07:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/setup/wizard.rs
Comment on lines +3846 to +3849
// Clean up: empty values are treated as unset by env_or_override()
// at every layer (real env, runtime overrides, INJECTED_VARS).
crate::config::inject_single_var("NEARAI_API_KEY", "");

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cleanup sets NEARAI_API_KEY in the injected-vars overlay to an empty string via inject_single_var("NEARAI_API_KEY", ""). Unlike env_or_override(), optional_env() does not currently filter empty values for INJECTED_VARS, so this may leave global state that other tests/config resolution can observe as Some("") (and it can also race with parallel tests since ENV_MUTEX only guards process env). Prefer restoring the previous injected value, or provide a way to actually remove the key from the injected overlay (e.g., a test-only clear helper or making inject_single_var delete the entry when value is empty).

Suggested change
// Clean up: empty values are treated as unset by env_or_override()
// at every layer (real env, runtime overrides, INJECTED_VARS).
crate::config::inject_single_var("NEARAI_API_KEY", "");

Copilot uses AI. Check for mistakes.
Comment thread src/setup/README.md
```env
LLM_BACKEND="ollama"
OLLAMA_BASE_URL="http://localhost:11434"
SECRETS_MASTER_KEY="..."

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PostgreSQL bootstrap .env example, SECRETS_MASTER_KEY is shown without the “only if env key source selected” caveat that the libsql example includes. Since the master key may come from keychain mode (and therefore not be present in .env), this example can be read as implying it’s always required. Consider adding the same conditional note here for consistency and to avoid confusing operators.

Suggested change
SECRETS_MASTER_KEY="..."
SECRETS_MASTER_KEY="..." # only if env key source selected

Copilot uses AI. Check for mistakes.
Comment thread src/setup/README.md
- `ONBOARD_COMPLETED` (always, "true")
- Channel/sandbox vars: `CLAUDE_CODE_ENABLED`, `SIGNAL_HTTP_URL`, `SIGNAL_ACCOUNT`, etc. (channel init may precede DB)

LLM settings (`LLM_BACKEND`, `LLM_BASE_URL`, model, API keys) are persisted

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph says “LLM settings (LLM_BACKEND, LLM_BASE_URL, model, API keys) are persisted to the DB via persist_settings()…”, but API keys are not persisted via persist_settings() (they’re stored in the encrypted secrets DB). To avoid a misleading statement, consider removing “API keys” from the parenthetical list (or explicitly separating “settings persisted via persist_settings” from “credentials stored as secrets”).

Suggested change
LLM settings (`LLM_BACKEND`, `LLM_BASE_URL`, model, API keys) are persisted
LLM settings (`LLM_BACKEND`, `LLM_BASE_URL`, model) are persisted

Copilot uses AI. Check for mistakes.

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good -- correctly removes redundant plaintext LLM config from bootstrap .env, fixes the env_or_override() bug for model discovery during wizard, and adds a regression test. CI is green. Approving.

@ilblackdragon
ilblackdragon merged commit 9603fef into staging Mar 20, 2026
18 checks passed
@ilblackdragon
ilblackdragon deleted the fix/onboard-env-secrets-auth branch March 20, 2026 21:07
tianhaoz95 pushed a commit to tianhaoz95/clawgo that referenced this pull request Mar 22, 2026
…env (nearai#1448)

* fix(setup): remove redundant LLM vars and API keys from bootstrap .env

Only true chicken-and-egg vars belong in ~/.ironclaw/.env — things needed
to connect to the DB or decrypt secrets (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, SECRETS_MASTER_KEY, ONBOARD_COMPLETED).

LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name,
provider-specific URLs) are persisted to the DB via persist_settings()
and loaded by Config::from_db_with_toml() after connection. API keys are
stored encrypted in the secrets DB and injected via
inject_llm_keys_from_secrets(). Writing them as plaintext to .env was
redundant and a security regression.

Also fixes for_model_discovery() and build_nearai_model_fetch_config()
to use env_or_override() instead of std::env::var(), so they can read
NEARAI_API_KEY from the thread-safe overlay during the onboarding wizard
(where inject_single_var() sets the key after the user enters it).

Also fixes incorrect secret names in README (anthropic_api_key →
llm_anthropic_api_key, openai_api_key → llm_openai_api_key).

Supersedes nearai#266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing fallback_deliverable field to job_monitor tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: address review comments on bootstrap .env and README

- Update write_bootstrap_env() docstring to reflect current behavior
  (no LLM vars, no credentials)
- Fix Layer 1 .env examples in README to remove LLM_BACKEND/LLM_BASE_URL
- Fix legacy secret name in README example (anthropic_api_key →
  llm_anthropic_api_key)
- Document channel/sandbox vars in bootstrap vars list
- Add cleanup comment in test explaining empty-value-as-unset behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
…env (nearai#1448)

* fix(setup): remove redundant LLM vars and API keys from bootstrap .env

Only true chicken-and-egg vars belong in ~/.ironclaw/.env — things needed
to connect to the DB or decrypt secrets (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, SECRETS_MASTER_KEY, ONBOARD_COMPLETED).

LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name,
provider-specific URLs) are persisted to the DB via persist_settings()
and loaded by Config::from_db_with_toml() after connection. API keys are
stored encrypted in the secrets DB and injected via
inject_llm_keys_from_secrets(). Writing them as plaintext to .env was
redundant and a security regression.

Also fixes for_model_discovery() and build_nearai_model_fetch_config()
to use env_or_override() instead of std::env::var(), so they can read
NEARAI_API_KEY from the thread-safe overlay during the onboarding wizard
(where inject_single_var() sets the key after the user enters it).

Also fixes incorrect secret names in README (anthropic_api_key →
llm_anthropic_api_key, openai_api_key → llm_openai_api_key).

Supersedes nearai#266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing fallback_deliverable field to job_monitor tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: address review comments on bootstrap .env and README

- Update write_bootstrap_env() docstring to reflect current behavior
  (no LLM vars, no credentials)
- Fix Layer 1 .env examples in README to remove LLM_BACKEND/LLM_BASE_URL
- Fix legacy secret name in README example (anthropic_api_key →
  llm_anthropic_api_key)
- Document channel/sandbox vars in bootstrap vars list
- Add cleanup comment in test explaining empty-value-as-unset behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
…env (nearai#1448)

* fix(setup): remove redundant LLM vars and API keys from bootstrap .env

Only true chicken-and-egg vars belong in ~/.ironclaw/.env — things needed
to connect to the DB or decrypt secrets (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, SECRETS_MASTER_KEY, ONBOARD_COMPLETED).

LLM settings (LLM_BACKEND, LLM_BASE_URL, OLLAMA_BASE_URL, model name,
provider-specific URLs) are persisted to the DB via persist_settings()
and loaded by Config::from_db_with_toml() after connection. API keys are
stored encrypted in the secrets DB and injected via
inject_llm_keys_from_secrets(). Writing them as plaintext to .env was
redundant and a security regression.

Also fixes for_model_discovery() and build_nearai_model_fetch_config()
to use env_or_override() instead of std::env::var(), so they can read
NEARAI_API_KEY from the thread-safe overlay during the onboarding wizard
(where inject_single_var() sets the key after the user enters it).

Also fixes incorrect secret names in README (anthropic_api_key →
llm_anthropic_api_key, openai_api_key → llm_openai_api_key).

Supersedes nearai#266

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing fallback_deliverable field to job_monitor tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: address review comments on bootstrap .env and README

- Update write_bootstrap_env() docstring to reflect current behavior
  (no LLM vars, no credentials)
- Fix Layer 1 .env examples in README to remove LLM_BACKEND/LLM_BASE_URL
- Fix legacy secret name in README example (anthropic_api_key →
  llm_anthropic_api_key)
- Document channel/sandbox vars in bootstrap vars list
- Add cleanup comment in test explaining empty-value-as-unset behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: high Safety, secrets, auth, or critical infrastructure scope: agent Agent core (agent loop, router, scheduler) scope: docs Documentation scope: llm LLM integration scope: setup Onboarding / setup size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants