mirror of
https://github.com/khodges42/exoshell.git
synced 2026-06-14 18:08:37 +00:00
Ignore api key when local unless set specifically.
This commit is contained in:
parent
4a1054de9b
commit
3efb320f03
|
|
@ -44,6 +44,21 @@ enabled = true
|
||||||
directory = "transcripts"
|
directory = "transcripts"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Local OpenAI-compatible servers such as Ollama do not require an API key env var. Example Ollama config:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[provider]
|
||||||
|
base_url = "http://localhost:11434/v1"
|
||||||
|
model = "qwen3-coder-next"
|
||||||
|
|
||||||
|
[shell]
|
||||||
|
family = "powershell"
|
||||||
|
|
||||||
|
[transcript]
|
||||||
|
enabled = true
|
||||||
|
directory = "transcripts"
|
||||||
|
```
|
||||||
|
|
||||||
Supported shell families in this first pass are `powershell` and `posix`.
|
Supported shell families in this first pass are `powershell` and `posix`.
|
||||||
|
|
||||||
PowerShell example:
|
PowerShell example:
|
||||||
|
|
@ -72,7 +87,7 @@ Phase 1 behavior:
|
||||||
- Suggested command blocks are labeled for review in terminal output.
|
- Suggested command blocks are labeled for review in terminal output.
|
||||||
- Risky commands should be reviewed manually before use.
|
- Risky commands should be reviewed manually before use.
|
||||||
- Session transcripts are markdown files when transcripts are enabled.
|
- Session transcripts are markdown files when transcripts are enabled.
|
||||||
- API keys are read from environment variables and are not written to transcripts.
|
- Hosted provider API keys are read from environment variables and are not written to transcripts.
|
||||||
|
|
||||||
Current limitations:
|
Current limitations:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,14 +73,13 @@ impl Config {
|
||||||
let shell = raw.shell.unwrap_or_default();
|
let shell = raw.shell.unwrap_or_default();
|
||||||
let transcript = raw.transcript.unwrap_or_default();
|
let transcript = raw.transcript.unwrap_or_default();
|
||||||
|
|
||||||
|
let base_url = provider
|
||||||
|
.base_url
|
||||||
|
.unwrap_or_else(|| "https://api.openai.com/v1".into());
|
||||||
let api_key_env = provider
|
let api_key_env = provider
|
||||||
.api_key_env
|
.api_key_env
|
||||||
.unwrap_or_else(|| "OPENAI_API_KEY".into());
|
.unwrap_or_else(|| "OPENAI_API_KEY".into());
|
||||||
let api_key = env::var(&api_key_env).map_err(|_| {
|
let api_key = provider_api_key(&base_url, &api_key_env)?;
|
||||||
ConfigError::MissingApiKey(format!(
|
|
||||||
"set {api_key_env} or configure provider.api_key_env"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let family = shell.family.unwrap_or_else(default_shell_family);
|
let family = shell.family.unwrap_or_else(default_shell_family);
|
||||||
let family = family
|
let family = family
|
||||||
|
|
@ -89,9 +88,7 @@ impl Config {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
provider: ProviderConfig {
|
provider: ProviderConfig {
|
||||||
base_url: provider
|
base_url,
|
||||||
.base_url
|
|
||||||
.unwrap_or_else(|| "https://api.openai.com/v1".into()),
|
|
||||||
api_key,
|
api_key,
|
||||||
api_key_env,
|
api_key_env,
|
||||||
model: provider.model.unwrap_or_else(|| "gpt-4.1-mini".into()),
|
model: provider.model.unwrap_or_else(|| "gpt-4.1-mini".into()),
|
||||||
|
|
@ -145,6 +142,45 @@ fn default_shell_family() -> String {
|
||||||
ShellFamily::default_for_platform().to_string()
|
ShellFamily::default_for_platform().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn provider_api_key(base_url: &str, api_key_env: &str) -> Result<String, ConfigError> {
|
||||||
|
match env::var(api_key_env) {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(_) if is_local_provider_url(base_url) => Ok("exoshell-local-provider".into()),
|
||||||
|
Err(_) => Err(ConfigError::MissingApiKey(format!(
|
||||||
|
"set {api_key_env} or configure provider.api_key_env"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_local_provider_url(base_url: &str) -> bool {
|
||||||
|
let Some(host) = provider_host(base_url) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1" | "0.0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_host(base_url: &str) -> Option<String> {
|
||||||
|
let after_scheme = base_url
|
||||||
|
.strip_prefix("http://")
|
||||||
|
.or_else(|| base_url.strip_prefix("https://"))
|
||||||
|
.unwrap_or(base_url);
|
||||||
|
let authority = after_scheme.split('/').next()?.trim();
|
||||||
|
|
||||||
|
if authority.starts_with('[') {
|
||||||
|
return authority
|
||||||
|
.split(']')
|
||||||
|
.next()
|
||||||
|
.map(|host| host.trim_start_matches('[').to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
|
||||||
|
authority
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.filter(|host| !host.is_empty())
|
||||||
|
.map(|host| host.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
fn default_config_path() -> PathBuf {
|
fn default_config_path() -> PathBuf {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
env::var_os("APPDATA")
|
env::var_os("APPDATA")
|
||||||
|
|
@ -244,17 +280,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loads_toml_config_file() {
|
fn loads_toml_config_file() {
|
||||||
unsafe {
|
|
||||||
env::set_var("EXOSHELL_TEST_KEY", "secret");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = tempfile::NamedTempFile::new().expect("temp config");
|
let mut file = tempfile::NamedTempFile::new().expect("temp config");
|
||||||
write!(
|
write!(
|
||||||
file,
|
file,
|
||||||
r#"
|
r#"
|
||||||
[provider]
|
[provider]
|
||||||
base_url = "http://localhost:11434/v1"
|
base_url = "http://localhost:11434/v1"
|
||||||
api_key_env = "EXOSHELL_TEST_KEY"
|
|
||||||
model = "local-model"
|
model = "local-model"
|
||||||
|
|
||||||
[shell]
|
[shell]
|
||||||
|
|
@ -269,6 +300,7 @@ enabled = false
|
||||||
let config = Config::load(Some(file.path())).expect("config loads");
|
let config = Config::load(Some(file.path())).expect("config loads");
|
||||||
|
|
||||||
assert_eq!(config.provider.base_url, "http://localhost:11434/v1");
|
assert_eq!(config.provider.base_url, "http://localhost:11434/v1");
|
||||||
|
assert_eq!(config.provider.api_key, "exoshell-local-provider");
|
||||||
assert_eq!(config.provider.model, "local-model");
|
assert_eq!(config.provider.model, "local-model");
|
||||||
assert_eq!(config.shell.family, ShellFamily::Posix);
|
assert_eq!(config.shell.family, ShellFamily::Posix);
|
||||||
assert!(!config.transcript.enabled);
|
assert!(!config.transcript.enabled);
|
||||||
|
|
@ -303,4 +335,42 @@ enabled = false
|
||||||
assert!(!config.transcript.enabled);
|
assert!(!config.transcript.enabled);
|
||||||
assert_eq!(config.transcript.directory, tempdir);
|
assert_eq!(config.transcript.directory, tempdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_provider_urls_do_not_require_api_key_env() {
|
||||||
|
let config = Config::from_raw(RawConfig {
|
||||||
|
provider: Some(RawProviderConfig {
|
||||||
|
base_url: Some("http://127.0.0.1:11434/v1".into()),
|
||||||
|
api_key_env: Some("EXOSHELL_MISSING_LOCAL_KEY".into()),
|
||||||
|
..RawProviderConfig::default()
|
||||||
|
}),
|
||||||
|
..RawConfig::default()
|
||||||
|
})
|
||||||
|
.expect("local config should load without key");
|
||||||
|
|
||||||
|
assert_eq!(config.provider.api_key, "exoshell-local-provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hosted_provider_urls_require_api_key_env() {
|
||||||
|
let error = Config::from_raw(RawConfig {
|
||||||
|
provider: Some(RawProviderConfig {
|
||||||
|
base_url: Some("https://api.openai.com/v1".into()),
|
||||||
|
api_key_env: Some("EXOSHELL_MISSING_HOSTED_KEY".into()),
|
||||||
|
..RawProviderConfig::default()
|
||||||
|
}),
|
||||||
|
..RawConfig::default()
|
||||||
|
})
|
||||||
|
.expect_err("hosted config should require key");
|
||||||
|
|
||||||
|
assert!(matches!(error, ConfigError::MissingApiKey(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detects_local_provider_hosts() {
|
||||||
|
assert!(is_local_provider_url("http://localhost:11434/v1"));
|
||||||
|
assert!(is_local_provider_url("http://127.0.0.1:11434/v1"));
|
||||||
|
assert!(is_local_provider_url("http://[::1]:11434/v1"));
|
||||||
|
assert!(!is_local_provider_url("https://api.openai.com/v1"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user