Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
065fc81
feat(reborn): keychain-backed master key for local-dev, dotfile-first
henrypark133 Jul 17, 2026
b854598
feat(reborn-ingress): tokenized login route feeding the existing sess…
henrypark133 Jul 17, 2026
74a8557
feat(reborn-cli): serve derives webui user id from config, env overrides
henrypark133 Jul 17, 2026
4ee9701
test(reborn-cli): suppress OS keychain in lane-merged smoke spawns
henrypark133 Jul 17, 2026
78e6728
feat(reborn-cli): onboard prompts for provider/API key into the encry…
henrypark133 Jul 17, 2026
3f04025
feat(reborn-cli): wire onboarding journey end-to-end (login link, ser…
henrypark133 Jul 17, 2026
7022490
fix(reborn-cli): onboarding review fixes — bootable headless default,…
henrypark133 Jul 17, 2026
691f7f4
fix(reborn-cli): adapt login_link to Result-returning webui_token_fil…
henrypark133 Jul 17, 2026
4f74fa3
fix(reborn-cli): drop direct ironclaw_secrets dependency from onboard…
henrypark133 Jul 17, 2026
0acd7aa
fix(reborn-cli): onboarding review fixes round 2
henrypark133 Jul 17, 2026
293bc5e
fix(reborn-cli): let a stored LLM key resolve at serve boot
henrypark133 Jul 17, 2026
3c284e5
fix(reborn-cli): mount CLI-token login only for file-sourced tokens
henrypark133 Jul 17, 2026
b4fea55
refactor(reborn-onboard): extract llm_credentials.rs from onboard/mod.rs
henrypark133 Jul 17, 2026
9615855
feat(reborn-composition): add RebornProviderAdmin::menu_entries()
henrypark133 Jul 17, 2026
9a6e0dd
feat(reborn-onboard): numbered provider menu with conditional API key…
henrypark133 Jul 17, 2026
878c156
fix(reborn-onboard): drop openai_compatible from provider menu
henrypark133 Jul 17, 2026
ee142b3
test(reborn-cli): dedup serve-banner polling, fix silent read/blockin…
henrypark133 Jul 17, 2026
3faae9d
fix(reborn-cli): mark onboard's config-load fallback with silent-ok
henrypark133 Jul 17, 2026
275dbd9
fix(reborn-cli): redact bearer token from Debug, fail closed on non-U…
henrypark133 Jul 17, 2026
11286e1
fix(reborn-composition): stop keyless retry masking real LLM catalog …
henrypark133 Jul 17, 2026
38a5515
fix(reborn-webui-ingress): sanitize CliTokenLoginConfig::with_redirec…
henrypark133 Jul 17, 2026
ce0d8b4
fix(reborn-cli): address coderabbit review on onboard provider menu
henrypark133 Jul 17, 2026
bb8c000
test(reborn-cli,webui-ingress): close onboarding-journey coverage gaps
henrypark133 Jul 17, 2026
ff026e6
chore(reborn-composition): update pub-use snapshot for onboarding fac…
henrypark133 Jul 17, 2026
7b8b7bb
fix(reborn-cli): anchor service WorkingDirectory + report live servic…
henrypark133 Jul 17, 2026
4d23594
fix(reborn-cli): stop config init/onboard from implicitly seeding [ll…
henrypark133 Jul 17, 2026
41650cd
feat(reborn-composition): add RebornProviderAdmin::detect_env_llm
henrypark133 Jul 17, 2026
90419c5
feat(reborn-cli): onboard env-detect-and-confirm / silent-seed LLM step
henrypark133 Jul 17, 2026
44bcc14
test(reborn-cli): rewrite onboard/serve capstones for the de-seeded stub
henrypark133 Jul 17, 2026
1ce8862
fix(docker-reborn): stop shipping a baked-in [llm.default] stub
henrypark133 Jul 17, 2026
2d5481f
chore(reborn-composition): update pub-use snapshot for detect_env_llm…
henrypark133 Jul 17, 2026
93f4a1e
fix(reborn-cli): anchor service WorkingDirectory at <reborn_home>/wor…
henrypark133 Jul 17, 2026
ec0d60f
feat(onboard): arrow-key provider menu, live key/model probe, nearai …
henrypark133 Jul 17, 2026
b790b69
fix(reborn-webui-ingress): stamp operator capability on session mint,…
henrypark133 Jul 17, 2026
464ba69
style: prune narrative comments to codebase density (keep flow/edge b…
henrypark133 Jul 17, 2026
66a18bb
fix(reborn-composition): probe nearai candidates against the coded ba…
henrypark133 Jul 17, 2026
9ea3621
fix(reborn): nearai always resolves the cloud-api base URL
henrypark133 Jul 17, 2026
6e2ecde
fix(webui): resolve WebuiAuthenticator through crate-local re-export
henrypark133 Jul 17, 2026
b22b3d6
fix(reborn-cli): adapt CLI-token-login smoke tests to root-path serving
henrypark133 Jul 17, 2026
4e04c02
fix(reborn-onboard): apply final review-fix batch for PR #6174
henrypark133 Jul 17, 2026
1892593
fix(reborn-onboard): apply second review-fix batch for PR #6174
henrypark133 Jul 17, 2026
bbf8b7c
fix(reborn): apply stored LLM key to boot gateway via one reload chok…
henrypark133 Jul 17, 2026
d7f84ea
fix(reborn-onboard): store LLM keys in the runtime storage root serve…
henrypark133 Jul 18, 2026
5657f22
fix(reborn): address PR #6174 review threads A-E
henrypark133 Jul 18, 2026
e827f6c
Merge branch 'main' into reborn-onboard-pr-b
henrypark133 Jul 18, 2026
0805555
fix(reborn-onboard): persist env-detected keys, rename LocalDev* type…
henrypark133 Jul 18, 2026
2571a42
fix(reborn-cli): resolve stored-key fallback against the runtime stor…
henrypark133 Jul 18, 2026
a3f8a17
fix(reborn-composition,webui): cap NEAR AI login state, fix codex log…
henrypark133 Jul 18, 2026
fb32e8e
test(reborn): update composition pub-use snapshot for ratchet-mandate…
henrypark133 Jul 18, 2026
a8e0f22
fix(reborn): apply stored LLM key in reload for key-required providers
henrypark133 Jul 18, 2026
a946277
fix(reborn-onboard): master-key root check, env-accept disclosure, co…
henrypark133 Jul 18, 2026
88a3183
test(reborn): pin probe_candidate wiring through a live HTTP stub
henrypark133 Jul 18, 2026
bc161a4
fix(reborn): tighten codex-login finalize to absent-or-mismatch abort
henrypark133 Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 128 additions & 0 deletions crates/ironclaw_llm/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,31 @@ impl LlmConfig {
.unwrap_or_else(|| self.nearai.model.clone()),
}
}

/// Resolve the base URL of the backend `serve` actually boots with, when
Comment thread
henrypark133 marked this conversation as resolved.
/// the backend has one.
///
/// Mirrors `active_model_name`'s per-backend dispatch. Exists so callers
/// outside this crate (the boot-time resolved-LLM debug trace, tests)
/// can observe the base URL without reaching into backend-specific
/// fields directly. `bedrock` and `gemini_oauth` authenticate via the AWS
/// credential chain / a fixed Google OAuth endpoint rather than an
/// operator-configurable base URL, so they return `None`.
pub fn active_base_url(&self) -> Option<String> {
match self.backend.as_str() {
"nearai" | "near_ai" | "near" => Some(self.nearai.base_url.clone()),
"bedrock" | "aws_bedrock" | "aws" | "gemini_oauth" | "gemini-oauth" => None,
"openai_codex" | "openai-codex" | "codex" => self
.openai_codex
.as_ref()
.map(|cfg| cfg.api_base_url.clone()),
_ => self
.provider
.as_ref()
.map(|cfg| cfg.base_url.clone())
.or_else(|| Some(self.nearai.base_url.clone())),
}
}
}

/// NEAR AI configuration.
Expand Down Expand Up @@ -699,4 +724,107 @@ mod tests {
assert_eq!(cfg.session_path, PathBuf::from("/tmp/sess.json"));
assert_eq!(cfg.token_refresh_margin_secs, 60);
}

/// Minimal `LlmConfig` with every optional backend-specific config left
/// `None` — the caller sets `backend` and populates whichever field the
/// case under test dispatches on.
fn base_llm_config(backend: &str) -> LlmConfig {
LlmConfig {
backend: backend.to_string(),
session: SessionConfig::default(),
nearai: NearAiConfig {
model: "test-model".to_string(),
cheap_model: None,
base_url: "https://cloud-api.near.ai".to_string(),
api_key: None,
fallback_model: None,
max_retries: 0,
circuit_breaker_threshold: None,
circuit_breaker_recovery_secs: 30,
response_cache_enabled: false,
response_cache_ttl_secs: 3600,
response_cache_max_entries: 1000,
failover_cooldown_secs: 300,
failover_cooldown_threshold: 3,
smart_routing_cascade: true,
},
provider: None,
bedrock: None,
gemini_oauth: None,
openai_codex: None,
request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
cheap_model: None,
smart_routing_cascade: true,
max_retries: 0,
circuit_breaker_threshold: None,
circuit_breaker_recovery_secs: 30,
response_cache_enabled: false,
response_cache_ttl_secs: 3600,
response_cache_max_entries: 1000,
}
}

/// `active_base_url` dispatches per-backend, mirroring `active_model_name`:
/// nearai aliases resolve to the nearai base URL, bedrock/gemini_oauth
/// have none (fixed credential chain / OAuth endpoint), openai_codex
/// reads its own config (or `None` when unset), a registry-backed
/// provider reads its config, and an unknown backend with no provider
/// config falls back to the nearai base URL.
#[test]
fn active_base_url_dispatches_backend_aliases_and_fallbacks() {
for alias in ["nearai", "near_ai", "near"] {
let cfg = base_llm_config(alias);
assert_eq!(
cfg.active_base_url().as_deref(),
Some("https://cloud-api.near.ai")
);
}

for backend in [
"bedrock",
"aws_bedrock",
"aws",
"gemini_oauth",
"gemini-oauth",
] {
let cfg = base_llm_config(backend);
assert_eq!(cfg.active_base_url(), None);
}

let mut cfg = base_llm_config("openai_codex");
cfg.openai_codex = Some(OpenAiCodexConfig::build(
None,
None,
Some("https://codex.example".to_string()),
None,
None,
None,
));
assert_eq!(
cfg.active_base_url().as_deref(),
Some("https://codex.example")
);

let cfg_no_codex_config = base_llm_config("codex");
assert_eq!(cfg_no_codex_config.active_base_url(), None);

let mut cfg = base_llm_config("openai");
cfg.provider = Some(RegistryProviderConfig::generic(
ProviderProtocol::OpenAiCompletions,
"openai",
None,
"https://api.openai.com/v1",
"gpt-test",
));
assert_eq!(
cfg.active_base_url().as_deref(),
Some("https://api.openai.com/v1")
);

let cfg_unknown_no_provider = base_llm_config("some_unknown_backend");
assert_eq!(
cfg_unknown_no_provider.active_base_url().as_deref(),
Some("https://cloud-api.near.ai")
);
}
}
46 changes: 33 additions & 13 deletions crates/ironclaw_llm/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ impl ResolvedProviderConfig {
Self::Dedicated(config) => &config.model,
}
}

/// The resolved API key, if the provider carries one — the same value
/// (from env) that would otherwise only ever reach a live provider
/// client, exposed so a caller (onboard's env-detect step) can persist
/// it into the encrypted secret store rather than leaving it only in
/// the process's shell env.
pub fn api_key(&self) -> Option<&SecretString> {
match self {
Self::Registry(config) => config.api_key.as_ref(),
Self::Dedicated(config) => config.api_key.as_ref(),
}
}
}

/// Provider selection overrides supplied by a composition root.
Expand Down Expand Up @@ -420,7 +432,7 @@ fn apply_registry_provider_env(config: &mut RegistryProviderConfig) -> Result<()

fn nearai_config_from_env(chain: &ChainSettings) -> Result<NearAiConfig, LlmError> {
let api_key = nonempty_env("NEARAI_API_KEY").map(SecretString::from);
let base_url = default_nearai_base_url(api_key.is_some(), nonempty_env("NEARAI_BASE_URL"));
let base_url = default_nearai_base_url(nonempty_env("NEARAI_BASE_URL"));
Ok(build_nearai_config(
NearAiRuntimeFields {
model: nonempty_env("NEARAI_MODEL").unwrap_or_else(|| crate::DEFAULT_MODEL.to_string()),
Expand All @@ -443,7 +455,7 @@ fn nearai_config_from_dedicated(
} else {
Some(resolved.base_url.clone())
};
let base_url = default_nearai_base_url(api_key.is_some(), configured_base_url);
let base_url = default_nearai_base_url(configured_base_url);

Ok(build_nearai_config(
NearAiRuntimeFields {
Expand Down Expand Up @@ -487,19 +499,27 @@ fn build_nearai_config(fields: NearAiRuntimeFields, chain: &ChainSettings) -> Ne
}

pub const NEARAI_CLOUD_DEFAULT_BASE_URL: &str = "https://cloud-api.near.ai";
/// No longer used by [`default_nearai_base_url`] (nearai always defaults to
/// cloud regardless of key presence — see that function's doc comment).
/// Kept only because `session.rs`'s OAuth/session-token auth URL default and
/// v1 `src/` still reference it independently of provider-config base-URL
/// resolution.
pub const NEARAI_PRIVATE_DEFAULT_BASE_URL: &str = "https://private.near.ai";

pub fn default_nearai_base_url(
api_key_present: bool,
configured_base_url: Option<String>,
) -> String {
if let Some(base_url) = configured_base_url {
base_url
} else if api_key_present {
NEARAI_CLOUD_DEFAULT_BASE_URL.to_string()
} else {
NEARAI_PRIVATE_DEFAULT_BASE_URL.to_string()
}
/// Resolve the nearai provider's base URL: an explicit override always wins,
/// otherwise the cloud endpoint.
///
/// Used to key on API-key presence (cloud when a key was present, private
/// otherwise), to match the session-token-only private backend's implicit
/// no-key affordance. That coupling made resolution order load-bearing:
/// whichever step attached the key had to run *before* this was called, and
/// an operator-stored key (attached after resolution, from the secret store)
/// always missed the window and landed on the keyless private default. There
/// is now exactly one nearai default — cloud — so resolution order no longer
/// matters and every caller (probe, resolution, snapshot display) agrees
/// unconditionally.
pub fn default_nearai_base_url(configured_base_url: Option<String>) -> String {
configured_base_url.unwrap_or_else(|| NEARAI_CLOUD_DEFAULT_BASE_URL.to_string())
}

fn is_registry_protocol(protocol: ProviderProtocol) -> bool {
Expand Down
21 changes: 21 additions & 0 deletions crates/ironclaw_reborn_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ chrono = { version = "0.4", features = ["serde"] }
hex = "0.4.3"
clap = { version = "4", features = ["derive", "env"] }
clap_complete = "4.5.0"
# Masked (echo-suppressed) API-key prompt in `onboard`. Same crate v1's setup
# wizard uses for secret-input masking — reused here rather than adding a
# second terminal-masking dependency.
crossterm = "0.29"
dotenvy = "0.15"
ironclaw_reborn_composition = { path = "../ironclaw_reborn_composition", version = "0.1.0" }
ironclaw_reborn_config = { path = "../ironclaw_reborn_config", version = "0.1.0" }
Expand All @@ -118,6 +122,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.11"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "io-util", "io-std", "sync", "time"] }
tokio-util = { version = "0.7", features = ["rt"] }
tracing = "0.1"
Expand Down Expand Up @@ -145,6 +150,22 @@ ironclaw_reborn_composition = { path = "../ironclaw_reborn_composition", version
# second, non-serializing lock domain (the #6015 flake). Dev-only: the lock is
# reached solely from `#[cfg(test)]` code in `runtime::test_env`.
ironclaw_common = { path = "../ironclaw_common", version = "0.4.2" }
# Unconditional (not the feature-gated `dep:async-trait` optional dependency
# above): `commands::onboard::mod`'s write-first-then-config-ordering test
# implements a fake `ironclaw_secrets::SecretStore` whose `put` always fails,
# needing `#[async_trait::async_trait]` in every feature combination `cargo
# test` runs, not just under `webui-v2-beta`.
async-trait = "0.1"
# Same fake `SecretStore` impl takes `ResourceScope` / `SecretHandle` /
# `Timestamp` in its trait method signatures.
ironclaw_host_api = { path = "../ironclaw_host_api", version = "0.1.0" }
# Same fake `SecretStore` impl (`FailingSecretStore` in `commands::onboard::mod`'s
# tests) implements `ironclaw_secrets::SecretStore` directly; production
# onboarding code only ever touches secrets through
# `ironclaw_reborn_composition::LlmKeyStore`'s facade (`put_plaintext`), so
# this stays a dev-only dependency — see
# `crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs::reborn_cli_binary_crate_stays_separate_from_v1_root`.
ironclaw_secrets = { path = "../ironclaw_secrets", version = "0.1.0" }

[[bin]]
name = "ironclaw-reborn"
Expand Down
Loading
Loading