Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/caos-cli/src/bin/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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}"))?;
Expand Down
41 changes: 34 additions & 7 deletions crates/caos-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,8 +984,7 @@ pub fn prepare_queued_request(
queued_head: &str,
) -> Result<String, String> {
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)
}
Expand Down Expand Up @@ -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<Vec<ClientSecret>, 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")
}
Expand All @@ -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(|_| ())
}
Expand Down Expand Up @@ -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 || {
Expand Down Expand Up @@ -2060,8 +2088,7 @@ pub fn generate_conversation_title(
options: &TurnOptions,
first_message: &str,
) -> Result<String, String> {
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}"));
Expand Down
4 changes: 3 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
'';

Expand Down
8 changes: 8 additions & 0 deletions tests/chat-offline/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down