@@ -921,20 +921,113 @@ impl TerraphimRlm {
921921 ) ;
922922 log:: info!( "RLM auto-configure: ollama model={}" , ollama_model) ;
923923
924- // OpenRouter: API key from env or 1Password, free tier model
924+ // OpenRouter: API key from env or 1Password, free tier model.
925+ //
926+ // Two-stage guard against the 1Password-hang trap that affected non-tty
927+ // shells (agent contexts, CI, `... | terraphim_rlm`):
928+ // 1. Skip entirely if stdin/stderr aren't ttys AND no GUI display is
929+ // available. 1Password's desktop daemon cannot prompt for biometric
930+ // approval in such contexts, so the call would block forever.
931+ // 2. If we do try, bound the wait with `wait_timeout` (5s, generous —
932+ // `op read` is normally <500ms) so a hung daemon doesn't hang us.
933+ //
934+ // Set `RLM_OP_TIMEOUT_SECS=0` to disable the timeout (debugging only).
935+ // Set `RLM_SKIP_OP=1` to force-skip the 1Password lookup.
925936 let or_api_key = std:: env:: var ( "OPENROUTER_API_KEY" ) . ok ( ) . or_else ( || {
926- let output = std:: process:: Command :: new ( "op" )
937+ if std:: env:: var ( "RLM_SKIP_OP" ) . ok ( ) . as_deref ( ) == Some ( "1" ) {
938+ log:: debug!( "RLM auto-configure: RLM_SKIP_OP=1 set, skipping 1Password" ) ;
939+ return None ;
940+ }
941+ let stdin_is_tty = atty:: is ( atty:: Stream :: Stdin ) ;
942+ let stderr_is_tty = atty:: is ( atty:: Stream :: Stderr ) ;
943+ let has_display = std:: env:: var ( "DISPLAY" ) . is_ok ( )
944+ || std:: env:: var ( "WAYLAND_DISPLAY" ) . is_ok ( ) ;
945+ if ( !stdin_is_tty || !stderr_is_tty) && !has_display {
946+ log:: debug!(
947+ "RLM auto-configure: skipping 1Password (no tty, no display): \
948+ stdin_tty={}, stderr_tty={}, has_display={}",
949+ stdin_is_tty, stderr_is_tty, has_display
950+ ) ;
951+ return None ;
952+ }
953+ use std:: io:: Read ;
954+ use std:: process:: Stdio ;
955+ use std:: time:: Duration ;
956+ use wait_timeout:: ChildExt ;
957+
958+ let timeout_secs: u64 = std:: env:: var ( "RLM_OP_TIMEOUT_SECS" )
959+ . ok ( )
960+ . and_then ( |s| s. parse ( ) . ok ( ) )
961+ . unwrap_or ( 5 ) ;
962+ if timeout_secs == 0 {
963+ log:: debug!( "RLM auto-configure: RLM_OP_TIMEOUT_SECS=0, no timeout" ) ;
964+ }
965+
966+ let mut child = match std:: process:: Command :: new ( "op" )
927967 . args ( [
928968 "read" ,
929969 "--account" ,
930970 "my.1password.com" ,
931971 "op://Terraphim/OpenRouterTesting-api-key/password" ,
932972 ] )
933- . output ( )
934- . ok ( ) ?;
935- String :: from_utf8 ( output. stdout )
936- . ok ( )
937- . map ( |s| s. trim ( ) . to_string ( ) )
973+ . stdin ( Stdio :: null ( ) )
974+ . stdout ( Stdio :: piped ( ) )
975+ . stderr ( Stdio :: piped ( ) )
976+ . spawn ( )
977+ {
978+ Ok ( c) => c,
979+ Err ( e) => {
980+ log:: warn!( "RLM auto-configure: failed to spawn `op`: {}" , e) ;
981+ return None ;
982+ }
983+ } ;
984+
985+ // Returns Some(ExitStatus) on completion, None on timeout.
986+ // wait_timeout returns Result<Option<ExitStatus>, io::Error>;
987+ // the inner Option distinguishes "still running" (Some(None)) from
988+ // "errored" (Err). We collapse all "no usable status" cases to None.
989+ // Plain wait() returns Result<ExitStatus, io::Error>; lift to Option.
990+ let status_result: Option < std:: process:: ExitStatus > = if timeout_secs == 0 {
991+ child. wait ( ) . ok ( )
992+ } else {
993+ match child. wait_timeout ( Duration :: from_secs ( timeout_secs) ) {
994+ Ok ( Some ( status) ) => Some ( status) ,
995+ Ok ( None ) => {
996+ // Shouldn't happen with our usage but handle defensively
997+ log:: warn!( "RLM auto-configure: wait_timeout returned no status" ) ;
998+ let _ = child. kill ( ) ;
999+ let _ = child. wait ( ) ;
1000+ None
1001+ }
1002+ Err ( e) => {
1003+ log:: warn!( "RLM auto-configure: wait_timeout error: {}" , e) ;
1004+ let _ = child. kill ( ) ;
1005+ None
1006+ }
1007+ }
1008+ } ;
1009+ match status_result {
1010+ Some ( status) if status. success ( ) => {
1011+ let mut s = String :: new ( ) ;
1012+ if let Some ( mut stdout) = child. stdout . take ( ) {
1013+ let _ = stdout. read_to_string ( & mut s) ;
1014+ }
1015+ Some ( s. trim ( ) . to_string ( ) )
1016+ }
1017+ Some ( _) => {
1018+ log:: warn!( "RLM auto-configure: `op read` exited non-zero" ) ;
1019+ None
1020+ }
1021+ None => {
1022+ log:: warn!(
1023+ "RLM auto-configure: `op read` exceeded {}s timeout, killing" ,
1024+ timeout_secs
1025+ ) ;
1026+ let _ = child. kill ( ) ;
1027+ let _ = child. wait ( ) ;
1028+ None
1029+ }
1030+ }
9381031 } ) ;
9391032
9401033 if let Some ( ref key) = or_api_key {
0 commit comments