fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false),
      _ => true,
    };
    let id = entry.file_name().to_string_lossy().into_owned();
    let projects = projects_using_pool(paths, &id)?;

fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    let id = entry.file_name().to_string_lossy().into_owned();
    // oauth: Credentials liegen in claudes eigenem Keychain-Eintrag, dessen
    // Prüfung wäre ein security-Aufruf pro Pool im 3-s-Polling — immer true.
    // apikey: Store-Eintrag (nativer API-Call) oder Fallback-Datei.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => {
        store.has(&id)?
          || fs::read_to_string(entry.path().join(APIKEY_FILE))
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false)
      }
      _ => true,
    };
    let projects = projects_using_pool(paths, &id)?;

-fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {

+fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result<Vec<PoolInfo>, String> {

-    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein

-    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.

+    let id = entry.file_name().to_string_lossy().into_owned();

+    // oauth: Credentials liegen in claudes eigenem Keychain-Eintrag, dessen

+    // Prüfung wäre ein security-Aufruf pro Pool im 3-s-Polling — immer true.

+    // apikey: Store-Eintrag (nativer API-Call) oder Fallback-Datei.

-      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))

+          || fs::read_to_string(entry.path().join(APIKEY_FILE))

+            .map(|s| !s.trim().is_empty())

-    let id = entry.file_name().to_string_lossy().into_owned();

let running = projects.iter().filter(|p| is_running(p)).cloned().collect();


Edit

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
  if paths.pools_dir().is_dir() {
    for pool in list_pools_in(paths)? {
      if pool.name == name {
        return Err(format!("Pool existiert bereits: {name}"));
      }
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

/// (ID, Anzeigename) aller Pools. Beim ersten Pool existiert pools/ noch
/// nicht — dann ist die Liste leer.
fn pool_names(paths: &Paths) -> Result<Vec<(String, String)>, String> {
  let mut out = Vec::new();
  if !paths.pools_dir().is_dir() {
    return Ok(out);
  }
  for entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    out.push((entry.file_name().to_string_lossy().into_owned(), pool.name));
  }
  Ok(out)
}

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  if pool_names(paths)?.iter().any(|(_, n)| n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}


The file /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
  if paths.pools_dir().is_dir() {
    for pool in list_pools_in(paths)? {
      if pool.name == name {
        return Err(format!("Pool existiert bereits: {name}"));
      }
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

/// (ID, Anzeigename) aller Pools. Beim ersten Pool existiert pools/ noch
/// nicht — dann ist die Liste leer.
fn pool_names(paths: &Paths) -> Result<Vec<(String, String)>, String> {
  let mut out = Vec::new();
  if !paths.pools_dir().is_dir() {
    return Ok(out);
  }
  for entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    out.push((entry.file_name().to_string_lossy().into_owned(), pool.name));
  }
  Ok(out)
}

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  if pool_names(paths)?.iter().any(|(_, n)| n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

+/// (ID, Anzeigename) aller Pools. Beim ersten Pool existiert pools/ noch

+fn pool_names(paths: &Paths) -> Result<Vec<(String, String)>, String> {

+  for entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {

+    let entry = entry.map_err(|e| e.to_string())?;

+    let cfg_path = entry.path().join(POOL_FILE);

+    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;

+      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;

+    out.push((entry.file_name().to_string_lossy().into_owned(), pool.name));

-  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.

-    for pool in list_pools_in(paths)? {

-        return Err(format!("Pool existiert bereits: {name}"));

+  if pool_names(paths)?.iter().any(|(_, n)| n == name) {

+    return Err(format!("Pool existiert bereits: {name}"));


Benenne Pool privateDefault in private um


/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als
/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}


/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als
/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

-/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit

-/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.

-fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {

+/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als

+/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.

-  let key_path = dir.join(APIKEY_FILE);

-  write_secret_file(&key_path, &format!("{key}\n"))?;

+  let id = dir.file_name().unwrap().to_string_lossy().into_owned();

+    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;

-    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),

+    serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),

-  let id = dir.file_name().unwrap().to_string_lossy().into_owned();


fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  for other in list_pools_in(paths)? {
    if other.id != pool && other.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}

fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}


fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  for other in list_pools_in(paths)? {
    if other.id != pool && other.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}

fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}

-    if other.id != pool && other.name == name {

-      return Err(format!("Pool existiert bereits: {name}"));

+  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {

+    return Err(format!("Pool existiert bereits: {name}"));


/// Schreibt den API-Key eines apikey-Pools neu (0600).
fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))
}

/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).
/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }
  let settings_path = dir.join("settings.json");
  let raw = fs::read_to_string(&settings_path)
    .map_err(|e| format!("{}: {e}", settings_path.display()))?;
  let mut settings: serde_json::Value = serde_json::from_str(&raw)
    .map_err(|e| format!("{}: {e}", settings_path.display()))?;
  settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool));
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(&settings_path, raw + "\n").map_err(|e| e.to_string())
}


/// Schreibt den API-Key eines apikey-Pools neu (0600).
fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))
}

/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).
/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }
  let settings_path = dir.join("settings.json");
  let raw = fs::read_to_string(&settings_path)
    .map_err(|e| format!("{}: {e}", settings_path.display()))?;
  let mut settings: serde_json::Value = serde_json::from_str(&raw)
    .map_err(|e| format!("{}: {e}", settings_path.display()))?;
  settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool));
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(&settings_path, raw + "\n").map_err(|e| e.to_string())
}

-/// Schreibt den API-Key eines apikey-Pools neu (0600).

-fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {

+/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die

+/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).

+/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).

+/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.

-  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))

+  let key_path = dir.join(APIKEY_FILE);

+      fs::remove_file(&key_path).map_err(|e| e.to_string())?;

+    write_secret_file(&key_path, &format!("{key}\n"))?;

+  let settings_path = dir.join("settings.json");

+  let raw = fs::read_to_string(&settings_path)

+    .map_err(|e| format!("{}: {e}", settings_path.display()))?;

+  let mut settings: serde_json::Value = serde_json::from_str(&raw)

+  settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool));

+  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;

+  fs::write(&settings_path, raw + "\n").map_err(|e| e.to_string())

fn ensure_oauth_pool(paths: &Paths, pool: &str) -> Result<(), String> {


875	/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
876	/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
877	/// Liefert die Pool-ID.
878	fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
879	  let dir = check_new_pool(paths, name)?;
880	  init_pool_config(&dir, serde_json::json!({}))?;
881	  write_pool_json(&dir, name, "oauth")?;
882	  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
883	  link_pool_runtime_in(paths, &id)?;
884	  Ok(id)
885	}
886
887	/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
888	/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
889	fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
890	  check_name(name)?;
891	  let current = read_pool(paths, pool)?;
892	  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
893	    return Err(format!("Pool existiert bereits: {name}"));
894	  }
895	  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
896	}
897
898	/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
899	/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
900	/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
901	fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
902	  let dir = paths.pool_dir(name);
903	  if !dir.join(POOL_FILE).is_file() {
904	    return Err(format!("Pool nicht gefunden: {name}"));

/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
/// Liefert die Pool-ID.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}

/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));


/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));
  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

/// Löscht einen Pool samt Ordner (inkl. Credentials, bei apikey auch den
/// Keychain-Eintrag). Zugeordnete Projekte verlieren die Zuordnung
/// (Terminal-Einstellungen bleiben erhalten). Den Schutz gegen laufende
/// Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, store: &dyn ApikeyStore, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));
  }
  if read_pool(paths, name)?.credential_type == "apikey" {
    store.delete(name)?;
  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}


/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));
  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

/// Löscht einen Pool samt Ordner (inkl. Credentials, bei apikey auch den
/// Keychain-Eintrag). Zugeordnete Projekte verlieren die Zuordnung
/// (Terminal-Einstellungen bleiben erhalten). Den Schutz gegen laufende
/// Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, store: &dyn ApikeyStore, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));
  }
  if read_pool(paths, name)?.credential_type == "apikey" {
    store.delete(name)?;
  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

-/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte

-/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den

-/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.

-fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {

+/// Löscht einen Pool samt Ordner (inkl. Credentials, bei apikey auch den

+/// Keychain-Eintrag). Zugeordnete Projekte verlieren die Zuordnung

+/// (Terminal-Einstellungen bleiben erhalten). Den Schutz gegen laufende

+/// Sessions setzt der delete_pool-Command davor.

+fn delete_pool_in(paths: &Paths, store: &dyn ApikeyStore, name: &str) -> Result<(), String> {

+  if read_pool(paths, name)?.credential_type == "apikey" {


Jetzt die Command-Wrapper:


Bash

Command-Aufrufstellen finden


1204:  list_pools_in(&Paths::real())
1214:  create_apikey_pool_in(&Paths::real(), &name, &key)
1232:  delete_pool_in(&paths, &pool)
1262:  set_apikey_in(&Paths::real(), &pool, &key)
1948:    let pools = list_pools_in(&p).unwrap();
1954:    let pools = list_pools_in(&p).unwrap();
1958:    let pools = list_pools_in(&p).unwrap();
2038:    let pools = list_pools_in(&p).unwrap();


fn list_pools() -> Result<Vec<PoolInfo>, String> {

+  list_pools_in(&Paths::real(), &KeychainStore)


Edit

create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key)


fn create_apikey_pool(name: String, key: String) -> Result<String, String> {

-  create_apikey_pool_in(&Paths::real(), &name, &key)

+  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key)


+  delete_pool_in(&paths, &KeychainStore, &pool)


set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key)


fn set_apikey(pool: String, key: String) -> Result<(), String> {

-  set_apikey_in(&Paths::real(), &pool, &key)

+  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key)

/// Keychain-Service-Name des Pools: claude legt pro CLAUDE_CONFIG_DIR einen


Jetzt die Tests — Test-Stores und alle Aufrufstellen:


  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, name, key).unwrap()
  }

  /// In-Memory-ApikeyStore statt echtem Keychain.
  struct MapStore(std::sync::Mutex<HashMap<String, String>>);

  fn map_store() -> MapStore {
    MapStore(std::sync::Mutex::new(HashMap::new()))
  }

  impl ApikeyStore for MapStore {
    fn set(&self, pool: &str, key: &str) -> Result<(), String> {
      self.0.lock().unwrap().insert(pool.into(), key.into());
      Ok(())
    }
    fn has(&self, pool: &str) -> Result<bool, String> {
      Ok(self.0.lock().unwrap().contains_key(pool))
    }
    fn delete(&self, pool: &str) -> Result<(), String> {
      self.0.lock().unwrap().remove(pool);
      Ok(())
    }
  }

  /// Store ohne Keychain/Keyring — Schreiben scheitert, Lesen findet nichts.
  struct FailStore;

  impl ApikeyStore for FailStore {
    fn set(&self, _pool: &str, _key: &str) -> Result<(), String> {
      Err("kein Keychain".into())
    }
    fn has(&self, _pool: &str) -> Result<bool, String> {
      Ok(false)
    }
    fn delete(&self, _pool: &str) -> Result<(), String> {
      Ok(())
    }
  }

  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key).unwrap()
  }


  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, name, key).unwrap()
  }

  /// In-Memory-ApikeyStore statt echtem Keychain.
  struct MapStore(std::sync::Mutex<HashMap<String, String>>);

  fn map_store() -> MapStore {
    MapStore(std::sync::Mutex::new(HashMap::new()))
  }

  impl ApikeyStore for MapStore {
    fn set(&self, pool: &str, key: &str) -> Result<(), String> {
      self.0.lock().unwrap().insert(pool.into(), key.into());
      Ok(())
    }
    fn has(&self, pool: &str) -> Result<bool, String> {
      Ok(self.0.lock().unwrap().contains_key(pool))
    }
    fn delete(&self, pool: &str) -> Result<(), String> {
      self.0.lock().unwrap().remove(pool);
      Ok(())
    }
  }

  /// Store ohne Keychain/Keyring — Schreiben scheitert, Lesen findet nichts.
  struct FailStore;

  impl ApikeyStore for FailStore {
    fn set(&self, _pool: &str, _key: &str) -> Result<(), String> {
      Err("kein Keychain".into())
    }
    fn has(&self, _pool: &str) -> Result<bool, String> {
      Ok(false)
    }
    fn delete(&self, _pool: &str) -> Result<(), String> {
      Ok(())
    }
  }

  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key).unwrap()
  }

+  /// In-Memory-ApikeyStore statt echtem Keychain.

+  struct MapStore(std::sync::Mutex<HashMap<String, String>>);

+    MapStore(std::sync::Mutex::new(HashMap::new()))

+    fn set(&self, pool: &str, key: &str) -> Result<(), String> {

+      self.0.lock().unwrap().insert(pool.into(), key.into());

+    fn has(&self, pool: &str) -> Result<bool, String> {

+      Ok(self.0.lock().unwrap().contains_key(pool))

+    fn delete(&self, pool: &str) -> Result<(), String> {

+      self.0.lock().unwrap().remove(pool);

+  /// Store ohne Keychain/Keyring — Schreiben scheitert, Lesen findet nichts.

+    fn set(&self, _pool: &str, _key: &str) -> Result<(), String> {

+    fn has(&self, _pool: &str) -> Result<bool, String> {

+    fn delete(&self, _pool: &str) -> Result<(), String> {

-  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {

-    create_apikey_pool_in(p, name, key).unwrap()

+  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {

+    create_apikey_pool_in(p, store, name, key).unwrap()


Bash

grep -n "make_apikey_pool(\|create_apikey_pool_in(\|set_apikey_in(\|delete_pool_in(\|list_pools_in(&p" /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs | awk -F: '$1 > 1450'

Test-Aufrufstellen auflisten


1596:  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
1597:    create_apikey_pool_in(p, store, name, key).unwrap()
1609:    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
1672:    make_apikey_pool(&p, "kunde", "sk-1");
1673:    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
1682:    let id = make_apikey_pool(&p, "kunde", "sk-1");
1700:    let id = make_apikey_pool(&p, "kunde", "sk-1");
1711:    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
1717:    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
1725:    let id = make_apikey_pool(&p, "kunde", "sk-alt");
1726:    set_apikey_in(&p, &id, "sk-neu").unwrap();
1736:    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
1742:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1753:    let id = make_apikey_pool(&p, "kunde", "sk-1");
1754:    delete_pool_in(&p, &id).unwrap();
1763:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1918:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1956:    make_apikey_pool(&p, "kunde", "sk-1");
1981:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1984:    let pools = list_pools_in(&p).unwrap();
1990:    let pools = list_pools_in(&p).unwrap();
1994:    let pools = list_pools_in(&p).unwrap();
2001:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
2004:    delete_pool_in(&p, &kunde).unwrap();
2039:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
2048:    delete_pool_in(&p, &kunde).unwrap();
2057:    assert!(delete_pool_in(&p, "gibtsnicht").is_err());
2065:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
2074:    let pools = list_pools_in(&p).unwrap();
2088:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
2108:    let kunde = make_apikey_pool(&p, "kunde", "sk-1");


1601	    fs::metadata(path).unwrap().permissions().mode() & 0o777
1602	  }
1603
1604	  // -- Pool anlegen --
1605
1606	  #[test]
1607	  fn apikey_pool_anlegen() {
1608	    let p = tmp_paths();
1609	    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
1610	    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
1611	    assert!(uuid::Uuid::parse_str(&id).is_ok());
1612	    let dir = p.pool_dir(&id);
1613
1614	    let pool: Pool =
1615	      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
1616	    assert_eq!(pool.name, "kunde");
1617	    assert_eq!(pool.credential_type, "apikey");
1618
1619	    let key_path = dir.join(APIKEY_FILE);
1620	    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
1621	    assert_eq!(mode(&key_path), 0o600);
1622
1623	    let settings: serde_json::Value =
1624	      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
1625	        .unwrap();
1626	    assert_eq!(
1627	      settings["apiKeyHelper"].as_str().unwrap(),
1628	      format!("cat '{}'", key_path.display())
1629	    );
1630	  }
1631
1632	  #[test]
1633	  fn oauth_pool_anlegen() {
1634	    let p = tmp_paths();
1635	    let id = make_oauth_pool(&p, "privat");
1636	    let dir = p.pool_dir(&id);
1637
1638	    let pool: Pool =
1639	      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
1640	    assert_eq!(pool.credential_type, "oauth");
1641
1642	    // Kein Credentials-File — den Login macht claude selbst beim ersten Start.
1643	    assert!(!dir.join(".credentials.json").exists());
1644	    // Grundausstattung: settings.json mit aufgeräumten UI-Defaults + CLAUDE.md,
1645	    // damit claude nicht ins Onboarding fällt und keine Vorschlagstabelle zeigt.
1646	    let settings: serde_json::Value =
1647	      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
1648	        .unwrap();
1649	    assert_eq!(settings["promptSuggestionEnabled"], serde_json::json!(false));
1650	    assert_eq!(settings["awaySummaryEnabled"], serde_json::json!(false));
1651	    assert!(dir.join("CLAUDE.md").is_file());
1652	  }
1653
1654	  #[test]
1655	  fn pool_init_erhaelt_bestehende_claude_md() {
1656	    let p = tmp_paths();
1657	    let dir = p.pool_dir("privat");
1658	    fs::create_dir_all(&dir).unwrap();
1659	    fs::write(dir.join("CLAUDE.md"), "meine Vorgaben\n").unwrap();
1660	    // Anlegen darf die vorhandene CLAUDE.md nicht überschreiben.
1661	    // (check_new_pool erlaubt keinen bestehenden Ordner → direkt init testen)
1662	    init_pool_config(&dir, serde_json::json!({})).unwrap();
1663	    assert_eq!(
1664	      fs::read_to_string(dir.join("CLAUDE.md")).unwrap(),
1665	      "meine Vorgaben\n"
1666	    );
1667	  }
1668
1669	  #[test]
1670	  fn pool_doppelt_anlegen_scheitert() {
1671	    let p = tmp_paths();
1672	    make_apikey_pool(&p, "kunde", "sk-1");
1673	    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
1674	    assert!(err.contains("existiert bereits"));
1675	  }
1676
1677	  // -- Pool umbenennen --
1678
1679	  #[test]
1680	  fn pool_umbenennen() {
1681	    let p = tmp_paths();
1682	    let id = make_apikey_pool(&p, "kunde", "sk-1");
1683	    create_project_in(&p, "proj").unwrap();
1684	    assign_pool_in(&p, "proj", &id).unwrap();
1685
1686	    rename_pool_in(&p, &id, "kunde-neu").unwrap();
1687
1688	    // Nur der Anzeigename ändert sich — Ordner, Typ und Zuordnung bleiben.
1689	    let pool = read_pool(&p, &id).unwrap();
1690	    assert_eq!(pool.name, "kunde-neu");
1691	    assert_eq!(pool.credential_type, "apikey");
1692	    assert!(p.pool_dir(&id).is_dir());
1693	    let cfg = read_project_config_in(&p, "proj").unwrap();
1694	    assert_eq!(cfg.pool.as_deref(), Some(id.as_str()));
1695	  }
1696
1697	  #[test]
1698	  fn pool_umbenennen_auf_vergebenen_namen_scheitert() {
1699	    let p = tmp_paths();
1700	    let id = make_apikey_pool(&p, "kunde", "sk-1");
1701	    make_oauth_pool(&p, "privat");
1702	    let err = rename_pool_in(&p, &id, "privat").unwrap_err();
1703	    assert!(err.contains("existiert bereits"));
1704	    // Umbenennen auf den eigenen Namen ist erlaubt (No-op).
1705	    rename_pool_in(&p, &id, "kunde").unwrap();
1706	  }
1707
1708	  #[test]
1709	  fn pool_leerer_key_scheitert() {
1710	    let p = tmp_paths();
1711	    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
1712	  }
1713
1714	  #[test]
1715	  fn pool_name_mit_slash_scheitert() {
1716	    let p = tmp_paths();
1717	    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
1718	  }
1719
1720	  // -- Pool ändern --
1721
1722	  #[test]
1723	  fn apikey_aendern() {
1724	    let p = tmp_paths();
1725	    let id = make_apikey_pool(&p, "kunde", "sk-alt");
1726	    set_apikey_in(&p, &id, "sk-neu").unwrap();
1727	    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
1728	    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
1729	    assert_eq!(mode(&key_path), 0o600);
1730	  }
1731
1732	  #[test]
1733	  fn apikey_aendern_auf_oauth_pool_scheitert() {
1734	    let p = tmp_paths();
1735	    let id = make_oauth_pool(&p, "privat");
1736	    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
1737	  }
1738
1739	  #[test]
1740	  fn ensure_oauth_pool_auf_apikey_scheitert() {
1741	    let p = tmp_paths();
1742	    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1743	    assert!(ensure_oauth_pool(&p, &kunde).is_err());
1744	    let privat = make_oauth_pool(&p, "privat");
1745	    assert!(ensure_oauth_pool(&p, &privat).is_ok());
1746	  }
1747
1748	  // -- Pool löschen --
1749
1750	  #[test]
1751	  fn pool_loeschen() {
1752	    let p = tmp_paths();
1753	    let id = make_apikey_pool(&p, "kunde", "sk-1");
1754	    delete_pool_in(&p, &id).unwrap();
1755	    assert!(!p.pool_dir(&id).exists());
1756	  }
1757
1758	  // -- Projekt-Wizard --
1759
1760	  #[test]
1761	  fn projekt_wizard_scaffold() {
1762	    let p = tmp_paths();
1763	    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
1764	    create_project_full_in(
1765	      &p,

    fs::metadata(path).unwrap().permissions().mode() & 0o777
  }

  // -- Pool anlegen --

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      format!("cat '{}'", key_path.display())
    );
  }

  #[test]
  fn oauth_pool_anlegen() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.credential_type, "oauth");

    // Kein Credentials-File — den Login macht claude selbst beim ersten Start.
    assert!(!dir.join(".credentials.json").exists());
    // Grundausstattung: settings.json mit aufgeräumten UI-Defaults + CLAUDE.md,
    // damit claude nicht ins Onboarding fällt und keine Vorschlagstabelle zeigt.
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(settings["promptSuggestionEnabled"], serde_json::json!(false));
    assert_eq!(settings["awaySummaryEnabled"], serde_json::json!(false));
    assert!(dir.join("CLAUDE.md").is_file());
  }

  #[test]
  fn pool_init_erhaelt_bestehende_claude_md() {
    let p = tmp_paths();
    let dir = p.pool_dir("privat");
    fs::create_dir_all(&dir).unwrap();
    fs::write(dir.join("CLAUDE.md"), "meine Vorgaben\n").unwrap();
    // Anlegen darf die vorhandene CLAUDE.md nicht überschreiben.
    // (check_new_pool erlaubt keinen bestehenden Ordner → direkt init testen)
    init_pool_config(&dir, serde_json::json!({})).unwrap();
    assert_eq!(
      fs::read_to_string(dir.join("CLAUDE.md")).unwrap(),
      "meine Vorgaben\n"
    );
  }

  #[test]
  fn pool_doppelt_anlegen_scheitert() {
    let p = tmp_paths();
    make_apikey_pool(&p, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

  // -- Pool umbenennen --

  #[test]
  fn pool_umbenennen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

    // Nur der Anzeigename ändert sich — Ordner, Typ und Zuordnung bleiben.
    let pool = read_pool(&p, &id).unwrap();
    assert_eq!(pool.name, "kunde-neu");
    assert_eq!(pool.credential_type, "apikey");
    assert!(p.pool_dir(&id).is_dir());
    let cfg = read_project_config_in(&p, "proj").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(id.as_str()));
  }

  #[test]
  fn pool_umbenennen_auf_vergebenen_namen_scheitert() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();
    assert!(err.contains("existiert bereits"));
    // Umbenennen auf den eigenen Namen ist erlaubt (No-op).
    rename_pool_in(&p, &id, "kunde").unwrap();
  }

  #[test]
  fn pool_leerer_key_scheitert() {
    let p = tmp_paths();
    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
  }

  #[test]
  fn pool_name_mit_slash_scheitert() {
    let p = tmp_paths();
    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
  }

  // -- Pool ändern --

  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-alt");
    set_apikey_in(&p, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
  }

  #[test]
  fn ensure_oauth_pool_auf_apikey_scheitert() {
    let p = tmp_paths();
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());
    let privat = make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, &privat).is_ok());
  }

  // -- Pool löschen --

  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    delete_pool_in(&p, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
  }

  // -- Projekt-Wizard --

  #[test]
  fn projekt_wizard_scaffold() {
    let p = tmp_paths();
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_full_in(
      &p,


Edit

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      format!("cat '{}'", key_path.display())
    );
  }

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    // Key liegt im Store, keine Datei im Pool-Ordner.
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-test-123");
    assert!(!dir.join(APIKEY_FILE).exists());

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_pool_anlegen_ohne_store_faellt_auf_datei_zurueck() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-test-123");
    let dir = p.pool_dir(&id);

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    // Der Helper ist dieselbe Kette — der Store-Teil findet nichts,
    // cat liefert die Datei.
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_helper_kette_referenz() {
    let dir = PathBuf::from("/pools/abc");
    let cmd = apikey_helper_command(&dir, "abc");
    assert!(cmd.ends_with("|| cat '/pools/abc/apikey'"));
    #[cfg(target_os = "macos")]
    assert!(cmd.starts_with(
      "security find-generic-password -w -s ai-control-apikey -a abc 2>/dev/null"
    ));
    #[cfg(not(target_os = "macos"))]
    assert!(cmd.starts_with(
      "secret-tool lookup service ai-control-apikey username abc 2>/dev/null"
    ));
  }


  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      format!("cat '{}'", key_path.display())
    );
  }

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    // Key liegt im Store, keine Datei im Pool-Ordner.
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-test-123");
    assert!(!dir.join(APIKEY_FILE).exists());

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_pool_anlegen_ohne_store_faellt_auf_datei_zurueck() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-test-123");
    let dir = p.pool_dir(&id);

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    // Der Helper ist dieselbe Kette — der Store-Teil findet nichts,
    // cat liefert die Datei.
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_helper_kette_referenz() {
    let dir = PathBuf::from("/pools/abc");
    let cmd = apikey_helper_command(&dir, "abc");
    assert!(cmd.ends_with("|| cat '/pools/abc/apikey'"));
    #[cfg(target_os = "macos")]
    assert!(cmd.starts_with(
      "security find-generic-password -w -s ai-control-apikey -a abc 2>/dev/null"
    ));
    #[cfg(not(target_os = "macos"))]
    assert!(cmd.starts_with(
      "secret-tool lookup service ai-control-apikey username abc 2>/dev/null"
    ));
  }

-    let id = make_apikey_pool(&p, "kunde", "sk-test-123");

+    let id = make_apikey_pool(&p, &store, "kunde", "sk-test-123");

+    // Key liegt im Store, keine Datei im Pool-Ordner.

+    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-test-123");

+    assert!(!dir.join(APIKEY_FILE).exists());

+      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())

+      settings["apiKeyHelper"].as_str().unwrap(),

+  fn apikey_pool_anlegen_ohne_store_faellt_auf_datei_zurueck() {

+    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-test-123");

+    // Der Helper ist dieselbe Kette — der Store-Teil findet nichts,

-      format!("cat '{}'", key_path.display())

+    let dir = PathBuf::from("/pools/abc");

+    let cmd = apikey_helper_command(&dir, "abc");

+    assert!(cmd.ends_with("|| cat '/pools/abc/apikey'"));

+      "security find-generic-password -w -s ai-control-apikey -a abc 2>/dev/null"

+      "secret-tool lookup service ai-control-apikey username abc 2>/dev/null"


    make_apikey_pool(&p, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));

    let store = map_store();
    make_apikey_pool(&p, &store, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));


    make_apikey_pool(&p, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));

    let store = map_store();
    make_apikey_pool(&p, &store, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));

-    make_apikey_pool(&p, "kunde", "sk-1");

-    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();

+    make_apikey_pool(&p, &store, "kunde", "sk-1");

+    let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2").unwrap_err();


    let id = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

    let id = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();


    let id = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

    let id = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

-    let id = make_apikey_pool(&p, "kunde", "sk-1");

+    let id = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");


    let id = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();

    let id = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();


    let id = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();

    let id = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();


assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());

assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ").is_err());


-    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());

+    assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ").is_err());


assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1").is_err());


-    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());

+    assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1").is_err());


  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-alt");
    set_apikey_in(&p, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
  }

  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");
    assert!(!p.pool_dir(&id).join(APIKEY_FILE).exists());
  }

  /// Datei-Pool (Anlage ohne Store, z. B. Bestand vor der Keychain-Ablage):
  /// Key-Ändern hebt ihn in den Store, Datei und alter cat-Helper verschwinden.
  #[test]
  fn apikey_aendern_migriert_datei_in_store() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    let dir = p.pool_dir(&id);
    // Bestand simulieren: Helper wie vor der Keychain-Ablage.
    let old = serde_json::json!({ "apiKeyHelper": format!("cat '{}'", dir.join(APIKEY_FILE).display()) });
    fs::write(dir.join("settings.json"), old.to_string()).unwrap();

    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");
    assert!(!dir.join(APIKEY_FILE).exists());
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_aendern_ohne_store_schreibt_datei() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());
  }


  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-alt");
    set_apikey_in(&p, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
  }

  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");
    assert!(!p.pool_dir(&id).join(APIKEY_FILE).exists());
  }

  /// Datei-Pool (Anlage ohne Store, z. B. Bestand vor der Keychain-Ablage):
  /// Key-Ändern hebt ihn in den Store, Datei und alter cat-Helper verschwinden.
  #[test]
  fn apikey_aendern_migriert_datei_in_store() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    let dir = p.pool_dir(&id);
    // Bestand simulieren: Helper wie vor der Keychain-Ablage.
    let old = serde_json::json!({ "apiKeyHelper": format!("cat '{}'", dir.join(APIKEY_FILE).display()) });
    fs::write(dir.join("settings.json"), old.to_string()).unwrap();

    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");
    assert!(!dir.join(APIKEY_FILE).exists());
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      apikey_helper_command(&dir, &id)
    );
  }

  #[test]
  fn apikey_aendern_ohne_store_schreibt_datei() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());
  }

-    let id = make_apikey_pool(&p, "kunde", "sk-alt");

-    set_apikey_in(&p, &id, "sk-neu").unwrap();

+    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");

+    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

+    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

+    assert!(!p.pool_dir(&id).join(APIKEY_FILE).exists());

+  /// Datei-Pool (Anlage ohne Store, z. B. Bestand vor der Keychain-Ablage):

+  /// Key-Ändern hebt ihn in den Store, Datei und alter cat-Helper verschwinden.

+  fn apikey_aendern_migriert_datei_in_store() {

+    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");

+    // Bestand simulieren: Helper wie vor der Keychain-Ablage.

+    let old = serde_json::json!({ "apiKeyHelper": format!("cat '{}'", dir.join(APIKEY_FILE).display()) });

+    fs::write(dir.join("settings.json"), old.to_string()).unwrap();

+    assert!(!dir.join(APIKEY_FILE).exists());

+      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())

+      settings["apiKeyHelper"].as_str().unwrap(),

+  fn apikey_aendern_ohne_store_schreibt_datei() {

+    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();

-    assert!(set_apikey_in(&p, &id, "sk-1").is_err());

+    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());

-    let kunde = make_apikey_pool(&p, "kunde", "sk-1");

+    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");

let privat = make_oauth_pool(&p, "privat");


  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    delete_pool_in(&p, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
  }

  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-1");
    delete_pool_in(&p, &store, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
    // Der Keychain-Eintrag geht mit.
    assert!(!store.has(&id).unwrap());
  }


  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    delete_pool_in(&p, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
  }

  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    let store = map_store();
    let id = make_apikey_pool(&p, &store, "kunde", "sk-1");
    delete_pool_in(&p, &store, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
    // Der Keychain-Eintrag geht mit.
    assert!(!store.has(&id).unwrap());
  }

-    let id = make_apikey_pool(&p, "kunde", "sk-1");

+    let id = make_apikey_pool(&p, &store, "kunde", "sk-1");

+    delete_pool_in(&p, &store, &id).unwrap();


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_full_in(

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_full_in(


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_full_in(

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_full_in(


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");


    make_apikey_pool(&p, "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

    make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());


    make_apikey_pool(&p, "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

    make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

-    make_apikey_pool(&p, "kunde", "sk-1");

+    make_apikey_pool(&p, &map_store(), "kunde", "sk-1");


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

    // Datei-Pool (ohne Store angelegt): Datei entscheidet.
    let kunde = make_apikey_pool(&p, &FailStore, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

  /// Store-Pool: hasCredentials kommt aus dem Keychain-Eintrag, ohne Datei.
  #[test]
  fn pool_key_status_aus_store() {
    let p = tmp_paths();
    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    assert!(!p.pool_dir(&kunde).join(APIKEY_FILE).exists());
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    store.delete(&kunde).unwrap();
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

    // Datei-Pool (ohne Store angelegt): Datei entscheidet.
    let kunde = make_apikey_pool(&p, &FailStore, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

  /// Store-Pool: hasCredentials kommt aus dem Keychain-Eintrag, ohne Datei.
  #[test]
  fn pool_key_status_aus_store() {
    let p = tmp_paths();
    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    assert!(!p.pool_dir(&kunde).join(APIKEY_FILE).exists());
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    store.delete(&kunde).unwrap();
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

+    // Datei-Pool (ohne Store angelegt): Datei entscheidet.

+    let kunde = make_apikey_pool(&p, &FailStore, "kunde", "sk-1");

-    let pools = list_pools_in(&p).unwrap();

+    let pools = list_pools_in(&p, &FailStore).unwrap();

+  /// Store-Pool: hasCredentials kommt aus dem Keychain-Eintrag, ohne Datei.

+    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");

+    assert!(!p.pool_dir(&kunde).join(APIKEY_FILE).exists());

+    let pools = list_pools_in(&p, &store).unwrap();

+    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

+    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());

    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &store, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());

    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &store, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());

-    let kunde = make_apikey_pool(&p, "kunde", "sk-1");

-    delete_pool_in(&p, &kunde).unwrap();

+    delete_pool_in(&p, &store, &kunde).unwrap();


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(

    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(

    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(


    )
    .unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

    )
    .unwrap();
    delete_pool_in(&p, &store, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();


    )
    .unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

    )
    .unwrap();
    delete_pool_in(&p, &store, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

TerminalConfig { theme: Some("dracula".into()), icon: None, title: None },


-    assert!(delete_pool_in(&p, "gibtsnicht").is_err());

+    assert!(delete_pool_in(&p, &map_store(), "gibtsnicht").is_err());

// -- Projekt zuordnen / rausnehmen / wechseln --


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =

+    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();


    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();


    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();


    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();


Bash

CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -E "^error|test result|warning: unused" | head -20

Rust-Tests mit Keychain-Store laufen lassen


error: could not find `Cargo.toml` in `/Users/marcus.hinz/projects/ai-control` or any parent directory


cd /Users/marcus.hinz/projects/ai-control/src-tauri && CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -E "^error|test result|panicked" | head -20

Rust-Tests im src-tauri-Verzeichnis laufen lassen


error[E0061]: this function takes 2 arguments but 1 argument was supplied
error: could not compile `ai-control` (lib test) due to 1 previous error


CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -B3 -A6 "E0061" | head -20

Fehlerstelle anzeigen


   Compiling ai-control v0.1.0 (/Users/marcus.hinz/projects/ai-control/src-tauri)
error[E0061]: this function takes 2 arguments but 1 argument was supplied
    --> src/lib.rs:2167:17
     |
2167 |     let pools = list_pools_in(&p).unwrap();
     |                 ^^^^^^^^^^^^^---- argument #2 of type `&dyn ApikeyStore` is missing
     |
note: function defined here
--
2167 |     let pools = list_pools_in(&p, /* &dyn ApikeyStore */).unwrap();
     |                                 ++++++++++++++++++++++++

For more information about this error, try `rustc --explain E0061`.
error: could not compile `ai-control` (lib test) due to 1 previous error


2158	    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
2159	    create_project_in(&p, "proj").unwrap();
2160	    assign_pool_in(&p, "proj", &kunde).unwrap();
2161
2162	    let cfg: ProjectConfig =
2163	      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
2164	        .unwrap();
2165	    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));
2166
2167	    let pools = list_pools_in(&p).unwrap();
2168	    assert_eq!(pools[0].projects, vec!["proj"]);
2169	  }
2170
2171	  #[test]

    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));

    let pools = list_pools_in(&p).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);
  }

  #[test]


Edit

    let pools = list_pools_in(&p).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);

    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);


    let pools = list_pools_in(&p).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);

    let pools = list_pools_in(&p, &FailStore).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);

-    let pools = list_pools_in(&p).unwrap();

+    let pools = list_pools_in(&p, &FailStore).unwrap();


Bash

CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -E "^error|test result|panicked|^warning" | head -20

Rust-Tests erneut laufen lassen


test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


Edit

struct KeychainStore;

impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}

struct KeychainStore;

/// macOS über das security-CLI: dessen Einträge tragen /usr/bin/security in
/// der ACL, der apiKeyHelper (liest ebenfalls per security-CLI beim
/// claude-Start) kommt dadurch ohne Keychain-Prompt an den Key. Über das
/// Security-Framework angelegte Einträge (keyring-Crate) würden beim Lesen
/// durchs CLI prompten.
#[cfg(target_os = "macos")]
impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    let out = Command::new("security")
      .args(["add-generic-password", "-U", "-s", APIKEY_SERVICE, "-a", pool, "-w", key])
      .output()
      .map_err(|e| e.to_string())?;
    if out.status.success() {
      Ok(())
    } else {
      Err(String::from_utf8_lossy(&out.stderr).into_owned())
    }
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    // Ohne -w: nur Attribute, kein Secret — kein ACL-Prompt möglich.
    let out = Command::new("security")
      .args(["find-generic-password", "-s", APIKEY_SERVICE, "-a", pool])
      .output()
      .map_err(|e| e.to_string())?;
    Ok(out.status.success())
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    // Fehlender Eintrag ist kein Fehler — gelöscht ist gelöscht.
    Command::new("security")
      .args(["delete-generic-password", "-s", APIKEY_SERVICE, "-a", pool])
      .output()
      .map_err(|e| e.to_string())?;
    Ok(())
  }
}

#[cfg(not(target_os = "macos"))]
impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

#[cfg(not(target_os = "macos"))]
impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}


struct KeychainStore;

impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}

struct KeychainStore;

/// macOS über das security-CLI: dessen Einträge tragen /usr/bin/security in
/// der ACL, der apiKeyHelper (liest ebenfalls per security-CLI beim
/// claude-Start) kommt dadurch ohne Keychain-Prompt an den Key. Über das
/// Security-Framework angelegte Einträge (keyring-Crate) würden beim Lesen
/// durchs CLI prompten.
#[cfg(target_os = "macos")]
impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    let out = Command::new("security")
      .args(["add-generic-password", "-U", "-s", APIKEY_SERVICE, "-a", pool, "-w", key])
      .output()
      .map_err(|e| e.to_string())?;
    if out.status.success() {
      Ok(())
    } else {
      Err(String::from_utf8_lossy(&out.stderr).into_owned())
    }
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    // Ohne -w: nur Attribute, kein Secret — kein ACL-Prompt möglich.
    let out = Command::new("security")
      .args(["find-generic-password", "-s", APIKEY_SERVICE, "-a", pool])
      .output()
      .map_err(|e| e.to_string())?;
    Ok(out.status.success())
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    // Fehlender Eintrag ist kein Fehler — gelöscht ist gelöscht.
    Command::new("security")
      .args(["delete-generic-password", "-s", APIKEY_SERVICE, "-a", pool])
      .output()
      .map_err(|e| e.to_string())?;
    Ok(())
  }
}

#[cfg(not(target_os = "macos"))]
impl KeychainStore {
  fn entry(pool: &str) -> Result<keyring::Entry, String> {
    keyring::Entry::new(APIKEY_SERVICE, pool).map_err(|e| e.to_string())
  }
}

#[cfg(not(target_os = "macos"))]
impl ApikeyStore for KeychainStore {
  fn set(&self, pool: &str, key: &str) -> Result<(), String> {
    Self::entry(pool)?.set_password(key).map_err(|e| e.to_string())
  }

  fn has(&self, pool: &str) -> Result<bool, String> {
    match Self::entry(pool)?.get_password() {
      Ok(_) => Ok(true),
      // Kein Eintrag oder kein Store verfügbar → die Key-Datei entscheidet.
      Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(false),
      Err(e) => Err(e.to_string()),
    }
  }

  fn delete(&self, pool: &str) -> Result<(), String> {
    match Self::entry(pool)?.delete_credential() {
      Ok(())
      | Err(
        keyring::Error::NoEntry
        | keyring::Error::PlatformFailure(_)
        | keyring::Error::NoStorageAccess(_),
      ) => Ok(()),
      Err(e) => Err(e.to_string()),
    }
  }
}

+/// macOS über das security-CLI: dessen Einträge tragen /usr/bin/security in

+/// der ACL, der apiKeyHelper (liest ebenfalls per security-CLI beim

+/// claude-Start) kommt dadurch ohne Keychain-Prompt an den Key. Über das

+/// Security-Framework angelegte Einträge (keyring-Crate) würden beim Lesen

+  fn set(&self, pool: &str, key: &str) -> Result<(), String> {

+      .args(["add-generic-password", "-U", "-s", APIKEY_SERVICE, "-a", pool, "-w", key])

+      Err(String::from_utf8_lossy(&out.stderr).into_owned())

+  fn has(&self, pool: &str) -> Result<bool, String> {

+    // Ohne -w: nur Attribute, kein Secret — kein ACL-Prompt möglich.

+      .args(["find-generic-password", "-s", APIKEY_SERVICE, "-a", pool])

+  fn delete(&self, pool: &str) -> Result<(), String> {

+    // Fehlender Eintrag ist kein Fehler — gelöscht ist gelöscht.

+      .args(["delete-generic-password", "-s", APIKEY_SERVICE, "-a", pool])


Bash

CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -E "^error|test result|panicked|^warning" | head

Rust-Tests final laufen lassen


test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.72s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


security add-generic-password -U -s ai-control-apikey -a test-roundtrip -w sk-test-42 && security find-generic-password -w -s ai-control-apikey -a test-roundtrip && sh -c "security find-generic-password -w -s ai-control-apikey -a test-roundtrip 2>/dev/null || cat '/nicht/da'" && security delete-generic-password -s ai-control-apikey -a test-roundtrip >/dev/null && echo "gelöscht"

Keychain-Roundtrip: anlegen, lesen, Helper-Kette, löschen


sk-test-42
sk-test-42
password has been deleted.
gelöscht


Fertig und verifiziert.

**Ablage (`ApikeyStore`-Trait in `lib.rs`):**
- macOS: über das `security`-CLI (`add-/find-/delete-generic-password`), Service `ai-control-apikey`, Account = Pool-ID. Bewusst nicht über die keyring-Crate: deren Einträge hätten eine ACL nur für die App, und der `apiKeyHelper` liest per `security`-CLI — das würde beim claude-Start prompten. CLI-angelegte Einträge liest das CLI prompt-frei.
- Linux/Windows: keyring-Crate (Secret Service / Credential Manager), Attribute passen zum `secret-tool`-Lesen.
- Scheitert das Schreiben in den Store, landet der Key wie bisher als Datei (0600) im Pool-Ordner.

**Lesen:** `apiKeyHelper` ist jetzt die Kette `security find-generic-password -w … 2>/dev/null || cat '<datei>'` (Linux: `secret-tool lookup …`). Sie wird bei Anlage geschrieben und bei „Key ändern" aktualisiert — dabei wandert ein Datei-Key in den Store und die Datei wird gelöscht; apiDefault ist damit beim nächsten Key-Ändern migriert.

**Sonstiges:** `delete_pool` räumt den Store-Eintrag mit ab; `hasCredentials` prüft Store-Eintrag (Attribut-Abfrage ohne Secret, kein ACL-Prompt) oder Datei. UI unverändert.

**Verifiziert:** 46 Rust-Tests grün (neu: Anlage in Store ohne Datei, Datei-Fallback, Migration beim Key-Ändern, Löschen räumt Eintrag, hasCredentials aus Store, Helper-Ketten-Referenz). Roundtrip gegen den echten Keychain lief prompt-frei: anlegen → lesen → Helper-Kette → löschen.

Ein Detail: beim Schreiben steht der Key als `-w`-Argument kurz in der Prozessliste — `security add-generic-password` bietet keinen stdin-Weg.


ok
wir sollten promptern, wenn wir den Key nict sicher ablegen können, und abbruch anbieten


Plan:

1. **Backend:** `create_apikey_pool` und `set_apikey` bekommen ein Flag `allowFile`. Schlägt der Store-Write fehl und `allowFile=false`, bricht der Command ab, bevor irgendetwas angelegt/geändert wird, mit Sentinel-Fehler `"keychain-unavailable"`. Mit `allowFile=true` greift der Datei-Fallback wie jetzt.
2. **UI (PoolList):** erster Versuch immer `allowFile=false`. Kommt der Sentinel zurück, Dialog: „Keychain/Keyring nicht verfügbar — Key ungesichert als Datei im Pool-Ordner ablegen?" mit „Abbrechen" (nichts passiert) und „Als Datei ablegen" (erneuter Aufruf mit `allowFile=true`). Eingaben bleiben dabei stehen.
3. **Tests:** FailStore + `allow_file=false` → Abbruch ohne angelegten Pool bzw. ohne Änderung; bestehende Fallback-Tests laufen mit `allow_file=true`.

Go?


ja


Edit

const APIKEY_SERVICE: &str = "ai-control-apikey";

const APIKEY_SERVICE: &str = "ai-control-apikey";

/// Fehler-Sentinel an die UI: Store nicht verfügbar und Datei-Ablage (noch)
/// nicht erlaubt — die UI fragt dann nach und wiederholt mit allow_file.
const KEYCHAIN_UNAVAILABLE: &str = "keychain-unavailable";


const APIKEY_SERVICE: &str = "ai-control-apikey";

/// Fehler-Sentinel an die UI: Store nicht verfügbar und Datei-Ablage (noch)
/// nicht erlaubt — die UI fragt dann nach und wiederholt mit allow_file.
const KEYCHAIN_UNAVAILABLE: &str = "keychain-unavailable";

/// mit denselben Attributen über secret-tool.

+/// Fehler-Sentinel an die UI: Store nicht verfügbar und Datei-Ablage (noch)

+/// nicht erlaubt — die UI fragt dann nach und wiederholt mit allow_file.

+const KEYCHAIN_UNAVAILABLE: &str = "keychain-unavailable";

/// macOS über das security-CLI: dessen Einträge tragen /usr/bin/security in


/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als
/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }

/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 nur mit
/// allow_file), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Ohne Store und ohne allow_file bricht die Anlage ab, bevor etwas entsteht.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
  allow_file: bool,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    if !allow_file {
      return Err(KEYCHAIN_UNAVAILABLE.into());
    }
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }


/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als
/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }

/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 nur mit
/// allow_file), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Ohne Store und ohne allow_file bricht die Anlage ab, bevor etwas entsteht.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
  allow_file: bool,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    if !allow_file {
      return Err(KEYCHAIN_UNAVAILABLE.into());
    }
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }

-/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 als

-/// Fallback), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.

+/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 nur mit

+/// allow_file), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.

+/// Ohne Store und ohne allow_file bricht die Anlage ab, bevor etwas entsteht.

+      return Err(KEYCHAIN_UNAVAILABLE.into());


/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).
/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }

/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store: mit allow_file in die Datei (0600), sonst Abbruch
/// ohne Änderung. Der apiKeyHelper wird auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
  allow_file: bool,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    if !allow_file {
      return Err(KEYCHAIN_UNAVAILABLE.into());
    }
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }


/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).
/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }

/// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die
/// Fallback-Datei wird dabei entfernt (migriert Datei-Pools beim Key-Ändern).
/// Ohne verfügbaren Store: mit allow_file in die Datei (0600), sonst Abbruch
/// ohne Änderung. Der apiKeyHelper wird auf die aktuelle Kette gehoben.
fn set_apikey_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  pool: &str,
  key: &str,
  allow_file: bool,
) -> Result<(), String> {
  let p = read_pool(paths, pool)?;
  if p.credential_type != "apikey" {
    return Err(format!("Pool {pool} ist kein apikey-Pool"));
  }
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let dir = paths.pool_dir(pool);
  let key_path = dir.join(APIKEY_FILE);
  if store.set(pool, key).is_ok() {
    if key_path.is_file() {
      fs::remove_file(&key_path).map_err(|e| e.to_string())?;
    }
  } else {
    if !allow_file {
      return Err(KEYCHAIN_UNAVAILABLE.into());
    }
    write_secret_file(&key_path, &format!("{key}\n"))?;
  }

-/// Ohne verfügbaren Store landet der Key wie bisher in der Datei (0600).

-/// Der apiKeyHelper wird in beiden Fällen auf die aktuelle Kette gehoben.

+/// Ohne verfügbaren Store: mit allow_file in die Datei (0600), sonst Abbruch

+/// ohne Änderung. Der apiKeyHelper wird auf die aktuelle Kette gehoben.

let settings_path = dir.join("settings.json");


Command-Wrapper:


fn create_apikey_pool(name: String, key: String) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key)
}

fn create_apikey_pool(name: String, key: String, allow_file: bool) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key, allow_file)
}


fn create_apikey_pool(name: String, key: String) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key)
}

fn create_apikey_pool(name: String, key: String, allow_file: bool) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key, allow_file)
}

-fn create_apikey_pool(name: String, key: String) -> Result<String, String> {

-  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key)

+fn create_apikey_pool(name: String, key: String, allow_file: bool) -> Result<String, String> {

+  create_apikey_pool_in(&Paths::real(), &KeychainStore, &name, &key, allow_file)


fn set_apikey(pool: String, key: String) -> Result<(), String> {
  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key)
}

fn set_apikey(pool: String, key: String, allow_file: bool) -> Result<(), String> {
  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key, allow_file)
}


fn set_apikey(pool: String, key: String) -> Result<(), String> {
  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key)
}

fn set_apikey(pool: String, key: String, allow_file: bool) -> Result<(), String> {
  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key, allow_file)
}

-fn set_apikey(pool: String, key: String) -> Result<(), String> {

-  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key)

+fn set_apikey(pool: String, key: String, allow_file: bool) -> Result<(), String> {

+  set_apikey_in(&Paths::real(), &KeychainStore, &pool, &key, allow_file)

/// Keychain-Service-Name des Pools: claude legt pro CLAUDE_CONFIG_DIR einen


Tests anpassen und die zwei Abbruch-Tests ergänzen:


  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key).unwrap()
  }

  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key, true).unwrap()
  }