Karrot commited on
Commit
91ec8fe
·
unverified ·
1 Parent(s): e6ab538

feat: disabling keyring using --no-keyring (#308)

Browse files
docs/en-US/how-to/run-gui-headless-and-mcp.md CHANGED
@@ -141,3 +141,27 @@ koharu.exe --debug
141
  ```
142
 
143
  On Windows, debug and headless runs also influence how Koharu attaches to or creates a console window.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  ```
142
 
143
  On Windows, debug and headless runs also influence how Koharu attaches to or creates a console window.
144
+
145
+ ## Running without keyring
146
+
147
+ By default Koharu stores API keys in the system keyring. In container or CI environments where there is no persistent keyring, you can pass `--no-keyring` to skip it and supply API keys through environment variables instead.
148
+
149
+ The variable name for each provider follows the pattern `KOHARU_<PROVIDER>_API_KEY`:
150
+
151
+ | Provider | Environment variable |
152
+ | --- | --- |
153
+ | OpenAI | `KOHARU_OPENAI_API_KEY` |
154
+ | Gemini | `KOHARU_GEMINI_API_KEY` |
155
+ | Claude | `KOHARU_CLAUDE_API_KEY` |
156
+ | DeepSeek | `KOHARU_DEEPSEEK_API_KEY` |
157
+ | OpenAI-compatible | `KOHARU_OPENAI_COMPATIBLE_API_KEY` |
158
+
159
+ Hyphens in provider IDs are converted to underscores in the variable name.
160
+
161
+ Example for a headless container run:
162
+
163
+ ```bash
164
+ KOHARU_OPENAI_API_KEY=sk-... koharu --port 9999 --headless --no-keyring
165
+ ```
166
+
167
+ When `--no-keyring` is active, calls to save an API key through the UI or HTTP API are ignored.
docs/en-US/reference/cli.md CHANGED
@@ -31,6 +31,7 @@ koharu.exe [OPTIONS]
31
  | `--cpu` | Force CPU mode even when a GPU is available |
32
  | `-p`, `--port <PORT>` | Bind the local HTTP server to a specific `127.0.0.1` port instead of a random one |
33
  | `--headless` | Run without starting the desktop GUI |
 
34
  | `--debug` | Enable debug-oriented console output |
35
 
36
  ## Behavior notes
@@ -41,6 +42,7 @@ Some flags change more than just startup appearance:
41
  - with `--headless`, Koharu skips the Tauri window but still serves the Web UI and API
42
  - with `--download`, Koharu exits after dependency prefetch and does not stay running
43
  - with `--cpu`, both the vision stack and local LLM path avoid GPU acceleration
 
44
 
45
  When a fixed port is set, the main local endpoints are:
46
 
@@ -85,3 +87,9 @@ Start with explicit debug logging:
85
  ```bash
86
  koharu --debug
87
  ```
 
 
 
 
 
 
 
31
  | `--cpu` | Force CPU mode even when a GPU is available |
32
  | `-p`, `--port <PORT>` | Bind the local HTTP server to a specific `127.0.0.1` port instead of a random one |
33
  | `--headless` | Run without starting the desktop GUI |
34
+ | `--no-keyring` | Run without keyring and use environment variables instead |
35
  | `--debug` | Enable debug-oriented console output |
36
 
37
  ## Behavior notes
 
42
  - with `--headless`, Koharu skips the Tauri window but still serves the Web UI and API
43
  - with `--download`, Koharu exits after dependency prefetch and does not stay running
44
  - with `--cpu`, both the vision stack and local LLM path avoid GPU acceleration
45
+ - with `--no-keyring`, Koharu skips all keyring operations, API keys must be set via environment variables
46
 
47
  When a fixed port is set, the main local endpoints are:
48
 
 
87
  ```bash
88
  koharu --debug
89
  ```
90
+
91
+ Use without keyring
92
+
93
+ ```bash
94
+ KOHARU_OPENAI_API_KEY=[key] koharu --no-keyring
95
+ ```
koharu-llm/src/providers/mod.rs CHANGED
@@ -1,6 +1,7 @@
1
  use std::future::Future;
2
  use std::pin::Pin;
3
  use std::sync::Arc;
 
4
 
5
  use anyhow::Context;
6
  use keyring::Entry;
@@ -16,12 +17,33 @@ pub mod openai_compatible;
16
 
17
  const API_KEY_SERVICE: &str = "koharu";
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  fn provider_key_entry(provider: &str) -> anyhow::Result<Entry> {
20
  let username = format!("llm_provider_api_key_{provider}");
21
  Ok(Entry::new(API_KEY_SERVICE, &username)?)
22
  }
23
 
24
  pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
 
 
 
 
 
 
 
 
25
  let entry = provider_key_entry(provider)?;
26
  match entry.get_password() {
27
  Ok(value) => Ok(Some(value)),
@@ -31,6 +53,16 @@ pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
31
  }
32
 
33
  pub fn set_saved_api_key(provider: &str, api_key: &str) -> anyhow::Result<()> {
 
 
 
 
 
 
 
 
 
 
34
  let entry = provider_key_entry(provider)?;
35
  if api_key.trim().is_empty() {
36
  match entry.delete_credential() {
 
1
  use std::future::Future;
2
  use std::pin::Pin;
3
  use std::sync::Arc;
4
+ use std::sync::atomic::{AtomicBool, Ordering};
5
 
6
  use anyhow::Context;
7
  use keyring::Entry;
 
17
 
18
  const API_KEY_SERVICE: &str = "koharu";
19
 
20
+ static NO_KEYRING: AtomicBool = AtomicBool::new(false);
21
+
22
+ pub fn disable_keyring() {
23
+ NO_KEYRING.store(true, Ordering::Relaxed);
24
+ }
25
+
26
+ fn env_key_var(provider: &str) -> String {
27
+ format!(
28
+ "KOHARU_{}_API_KEY",
29
+ provider.to_ascii_uppercase().replace('-', "_")
30
+ )
31
+ }
32
+
33
  fn provider_key_entry(provider: &str) -> anyhow::Result<Entry> {
34
  let username = format!("llm_provider_api_key_{provider}");
35
  Ok(Entry::new(API_KEY_SERVICE, &username)?)
36
  }
37
 
38
  pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
39
+ if NO_KEYRING.load(Ordering::Relaxed) {
40
+ let var = env_key_var(provider);
41
+ return Ok(std::env::var(&var)
42
+ .ok()
43
+ .map(|v| v.trim().to_string())
44
+ .filter(|v| !v.is_empty()));
45
+ }
46
+
47
  let entry = provider_key_entry(provider)?;
48
  match entry.get_password() {
49
  Ok(value) => Ok(Some(value)),
 
53
  }
54
 
55
  pub fn set_saved_api_key(provider: &str, api_key: &str) -> anyhow::Result<()> {
56
+ if NO_KEYRING.load(Ordering::Relaxed) {
57
+ tracing::warn!(
58
+ provider,
59
+ "keyring is disabled; API key changes are not saved"
60
+ );
61
+ return Err(anyhow::anyhow!(
62
+ "keyring is disabled; API key cannot be saved"
63
+ ));
64
+ }
65
+
66
  let entry = provider_key_entry(provider)?;
67
  if api_key.trim().is_empty() {
68
  match entry.delete_credential() {
koharu/src/app.rs CHANGED
@@ -51,6 +51,12 @@ struct Cli {
51
  default_value_t = false
52
  )]
53
  headless: bool,
 
 
 
 
 
 
54
  #[arg(
55
  long,
56
  help = "Enable debug mode with console output",
@@ -356,9 +362,14 @@ pub async fn run() -> Result<()> {
356
  cpu,
357
  port,
358
  headless,
 
359
  debug,
360
  } = Cli::parse();
361
 
 
 
 
 
362
  initialize(headless, debug)?;
363
 
364
  if download {
 
51
  default_value_t = false
52
  )]
53
  headless: bool,
54
+ #[arg(
55
+ long,
56
+ help = "Disable keyring and read API keys from environment variables instead (e.g. KOHARU_OPENAI_API_KEY)",
57
+ default_value_t = false
58
+ )]
59
+ no_keyring: bool,
60
  #[arg(
61
  long,
62
  help = "Enable debug mode with console output",
 
362
  cpu,
363
  port,
364
  headless,
365
+ no_keyring,
366
  debug,
367
  } = Cli::parse();
368
 
369
+ if no_keyring {
370
+ koharu_llm::providers::disable_keyring();
371
+ }
372
+
373
  initialize(headless, debug)?;
374
 
375
  if download {