From 25483fd3463292e0d8e2401074b589826f556f60 Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 20 Aug 2026 13:39:28 -0700 Subject: [PATCH] Guide TUI users through secret setup --- crates/caos-cli/src/bin/tui/mod.rs | 9 +++++-- crates/caos-cli/src/lib.rs | 41 +++++++++++++++++++++++++----- flake.nix | 4 ++- tests/chat-offline/cli.sh | 8 ++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/crates/caos-cli/src/bin/tui/mod.rs b/crates/caos-cli/src/bin/tui/mod.rs index 946cf83a..08956d8c 100644 --- a/crates/caos-cli/src/bin/tui/mod.rs +++ b/crates/caos-cli/src/bin/tui/mod.rs @@ -4,7 +4,10 @@ use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use caos::GitTransport; -use caos_cli::{list_user_conversations, unarchive_user_conversation, UserConversationStatus}; +use caos_cli::{ + ensure_conversation_secret, list_user_conversations, unarchive_user_conversation, + UserConversationStatus, +}; use ratatui_core::layout::Rect; use ratatui_core::terminal::Terminal; use ratatui_crossterm::crossterm::event::{ @@ -246,7 +249,9 @@ pub(crate) fn run(raw: &[String]) -> Result<(), String> { if !io::stdin().is_terminal() || !io::stdout().is_terminal() { return Err("requires an interactive terminal; use `caos talk` for pipes".to_string()); } - GitTransport::from_cwd()?.ensure_server_reachable()?; + let transport = GitTransport::from_cwd()?; + transport.ensure_server_reachable()?; + ensure_conversation_secret(&transport)?; let mut app = App::new(args)?; enable_raw_mode().map_err(|error| format!("enabling terminal raw mode: {error}"))?; diff --git a/crates/caos-cli/src/lib.rs b/crates/caos-cli/src/lib.rs index 517ca8e4..3656ec61 100644 --- a/crates/caos-cli/src/lib.rs +++ b/crates/caos-cli/src/lib.rs @@ -984,8 +984,7 @@ pub fn prepare_queued_request( queued_head: &str, ) -> Result { validate_hash(queued_head, "queued conversation head")?; - let store = build_secret_store(t)?; - require_model_secret(&store)?; + let store = conversation_secret_store(t)?; let llm = resolve_llm(t, options, id, &store)?; prepare_client_request_with_store(t, &llm, &[format!("--head:commit={queued_head}")], &store) } @@ -1103,11 +1102,40 @@ fn require_model_secret(store: &[ClientSecret]) -> Result<(), String> { if store.iter().any(|secret| secret.name() == MODEL_API_SECRET) { return Ok(()); } + // The shipped `caos`/`caos-cli` wrapper records its runtime $0 before + // replacing argv[0] with the stable name used by usage diagnostics. That + // keeps this recovery command bound to the checkout or profile binary the + // person actually invoked. Direct cargo-built binaries fall back to their + // own argv[0]. + let invoked_as = std::env::var_os("CAOS_INVOKED_AS") + .or_else(|| std::env::args_os().next()) + .filter(|command| !command.is_empty()) + .map(|command| command.to_string_lossy().into_owned()) + .unwrap_or_else(|| "caos-cli".to_string()); Err(format!( - "conversation needs a {MODEL_API_SECRET:?} secret in .caos-secrets" + "conversations need an Anthropic API key. Create the git-ignored file \ + `.caos-secrets/{MODEL_API_SECRET}` with:\n\n\ + name={MODEL_API_SECRET}\n\ + value:@=/absolute/path/to/your/anthropic-api-key\n\ + reader=DEEP-DEPS/llm-step\n\ + reader=DEEP-DEPS/llm-call\n\n\ + Then run `{invoked_as} secrets` to add cache-isolation entropy. \ + See the README's Secrets section for details." )) } +fn conversation_secret_store(t: &GitTransport) -> Result, String> { + let store = build_secret_store(t)?; + require_model_secret(&store)?; + Ok(store) +} + +/// Check the model credential before an interactive client takes over the +/// terminal, so setup failures remain readable at the shell prompt. +pub fn ensure_conversation_secret(t: &GitTransport) -> Result<(), String> { + conversation_secret_store(t).map(drop) +} + fn request_is_active(status: &str) -> bool { matches!(status, "queued" | "running") } @@ -1116,7 +1144,7 @@ fn request_is_active(status: &str) -> bool { /// conversation state; `llm-step` advances the canonical head itself. pub fn resume_request(t: &GitTransport, request: &str) -> Result<(), String> { validate_hash(request, "request")?; - let store = build_secret_store(t)?; + let store = conversation_secret_store(t)?; let server = t.server_url()?; compute_client_request_with_store(&server, request, &store).map(|_| ()) } @@ -1996,7 +2024,7 @@ pub fn run_chat_turn( if let Some(request) = request { emit(TurnEvent::PhaseStarted(TurnPhase::Model)); emit(TurnEvent::Status("waiting for agent".to_string())); - let store = build_secret_store(t)?; + let store = conversation_secret_store(t)?; let server = t.server_url()?; let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { @@ -2060,8 +2088,7 @@ pub fn generate_conversation_title( options: &TurnOptions, first_message: &str, ) -> Result { - let store = build_secret_store(t)?; - require_model_secret(&store)?; + let store = conversation_secret_store(t)?; let mut kvs = Vec::new(); if let Some(url) = &options.base_url { kvs.push(format!("--base-url={url}")); diff --git a/flake.nix b/flake.nix index 42bb23f0..78bec2e3 100644 --- a/flake.nix +++ b/flake.nix @@ -613,7 +613,9 @@ mkdir -p $out/bin for name in caos-cli caos; do makeWrapper ${caos-cli-bin}/bin/caos-cli $out/bin/$name \ - --argv0 $name --set-default CAOS_REV ${caosRev} + --argv0 $name \ + --set-default CAOS_REV ${caosRev} \ + --run 'export CAOS_INVOKED_AS="$0"' done ''; diff --git a/tests/chat-offline/cli.sh b/tests/chat-offline/cli.sh index ad4c21cb..6be88b32 100755 --- a/tests/chat-offline/cli.sh +++ b/tests/chat-offline/cli.sh @@ -107,6 +107,14 @@ if "$CAOS_CLI" chat "$queued_conv" -m "hello" --base "$base" "${opts[@]}" 2>key. fail "chat succeeded without its model secret" fi grep -q "anthropic-api-key" key.err || fail "missing-key error is unclear" +grep -qF '.caos-secrets/anthropic-api-key' key.err \ + || fail "missing-key error does not name the setup file" +grep -qF 'reader=DEEP-DEPS/llm-step' key.err \ + || fail "missing-key error does not explain the llm-step grant" +grep -qF 'reader=DEEP-DEPS/llm-call' key.err \ + || fail "missing-key error does not explain the title grant" +grep -qF "$CAOS_CLI secrets" key.err \ + || fail "missing-key error does not explain entropy setup" if remote_tip "$queued_ref" >/dev/null; then fail "request-preparation failure partially admitted a conversation" fi