6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 1 | use std::{ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 2 | io::{self, Read}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 3 | path::{Path, PathBuf}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 4 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 5 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 6 | use serde::Deserialize; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 7 | use serde_json::{Value, json}; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 8 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 9 | use crate::{ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 10 | artifacts, client, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 11 | config::{Config, StopPolicy, Unwrapped}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 12 | history, paths, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 13 | proto::{self, Event, LeaseInfo, Reader, Request}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 14 | units::Dur, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 15 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 16 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 17 | #[derive(Deserialize)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 18 | struct Input { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 19 | #[serde(default)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 20 | session_id: String, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 21 | #[serde(default)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 22 | cwd: Option<PathBuf>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 23 | #[serde(default)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 24 | tool_name: Option<String>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 25 | #[serde(default, rename = "tool_input")] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 26 | tool_args: Option<Value>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 27 | #[serde(default)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 28 | stop_hook_active: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 29 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 30 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 31 | fn read_input() -> Option<Input> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 32 | let mut buf = String::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 33 | io::stdin().read_to_string(&mut buf).ok()?; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 34 | serde_json::from_str(&buf).ok() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 35 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 36 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 37 | fn emit(v: &Value) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 38 | println!("{v}"); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 39 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 40 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 41 | /// quote-aware split on `&& || ; | & \n`. subshells, heredocs and friends get |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 42 | /// treated as words, which errs toward seeing a build that isn't there |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 43 | fn segments(cmd: &str) -> Vec<Vec<String>> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 44 | let mut out = Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 45 | let mut words = Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 46 | let mut word = String::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 47 | let mut quote: Option<char> = None; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 48 | let mut chars = cmd.chars().peekable(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 49 | let flush_word = |word: &mut String, words: &mut Vec<String>| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 50 | if !word.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 51 | words.push(std::mem::take(word)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 52 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 53 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 54 | while let Some(c) = chars.next() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 55 | match (quote, c) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 56 | (Some(q), c) if c == q => quote = None, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 57 | (Some('"') | None, '\\') => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 58 | if let Some(n) = chars.next() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 59 | word.push(n); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 60 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 61 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 62 | (None, '\'' | '"') => quote = Some(c), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 63 | (None, '&') if word.ends_with('>') || chars.peek() == Some(&'>') => word.push(c), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 64 | (None, '&' | '|' | ';' | '\n' | '(' | ')') => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 65 | flush_word(&mut word, &mut words); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 66 | if !words.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 67 | out.push(std::mem::take(&mut words)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 68 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 69 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 70 | (None, c) if c.is_whitespace() => flush_word(&mut word, &mut words), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 71 | (_, c) => word.push(c), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 72 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 73 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 74 | flush_word(&mut word, &mut words); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 75 | if !words.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 76 | out.push(words); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 77 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 78 | out |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 79 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 80 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 81 | /// peels env assignments and wrappers that don't change what runs |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 82 | fn head(seg: &[String]) -> &[String] { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 83 | let mut s = seg; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 84 | loop { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 85 | match s.first().map(String::as_str) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 86 | Some(w) if w.contains('=') && !w.starts_with('-') && !w.starts_with('=') => s = &s[1..], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 87 | Some("time" | "nice" | "nohup" | "command" | "exec" | "env" | "sudo" | "doas") => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 88 | s = &s[1..]; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 89 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 90 | Some("timeout") => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 91 | s = &s[1..]; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 92 | while s.first().is_some_and(|w| w.starts_with('-')) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 93 | s = &s[1..]; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 94 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 95 | s = s.get(1..).unwrap_or_default(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 96 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 97 | _ => return s, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 98 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 99 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 100 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 101 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 102 | fn is_rabt(word: &str) -> bool { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 103 | word == "rabt" || word.ends_with("/rabt") |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 104 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 105 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 106 | fn matches_pattern(seg: &[String], pattern: &str) -> bool { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 107 | let want: Vec<&str> = pattern.split_whitespace().collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 108 | let first_ok = |w: &str, p: &str| w == p || w.rsplit('/').next() == Some(p); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 109 | !want.is_empty() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 110 | && seg.len() >= want.len() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 111 | && first_ok(&seg[0], want[0]) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 112 | && seg[1..want.len()] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 113 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 114 | .zip(&want[1..]) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 115 | .all(|(w, p)| w == p) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 116 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 117 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 118 | enum Scan { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 119 | Clean, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 120 | Unwrapped(Vec<String>), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 121 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 122 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 123 | fn scan(cfg: &Config, cmd: &str) -> Scan { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 124 | let mut found = Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 125 | for seg in segments(cmd) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 126 | let seg = head(&seg); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 127 | if seg.first().is_some_and(|w| is_rabt(w)) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 128 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 129 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 130 | if cfg.detect_patterns().any(|p| matches_pattern(seg, p)) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 131 | let words: Vec<&str> = seg |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 132 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 133 | .map(String::as_str) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 134 | .take_while(|w| !w.contains('>') && !w.contains('<')) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 135 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 136 | found.push(words.join(" ")); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 137 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 138 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 139 | if found.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 140 | Scan::Clean |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 141 | } else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 142 | Scan::Unwrapped(found) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 143 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 144 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 145 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 146 | fn guess(cfg: &Config, cwd: &Path, cmd: &[String]) -> String { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 147 | let key = history::key(&paths::project_root(cwd), cmd); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 148 | history::lookup(&key).map_or_else( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 149 | || format!("--size {}", cfg.enforce.default_size), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 150 | |rec| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 151 | crate::run::base(cfg).map_or_else( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 152 | |_| format!("--size {}", cfg.enforce.default_size), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 153 | |b| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 154 | let n = history::suggest(&rec, b, cmd); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 155 | format!("--ram {} --cores {} --time {}", n.ram, n.cores, Dur(n.time)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 156 | }, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 157 | ) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 158 | }, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 159 | ) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 160 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 161 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 162 | fn shell_quote(s: &str) -> String { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 163 | format!("'{}'", s.replace('\'', r"'\''")) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 164 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 165 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 166 | fn pre_tool_use(cfg: &Config, input: Input) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 167 | if input.tool_name.as_deref() != Some("Bash") { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 168 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 169 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 170 | let Some(mut tool_input) = input.tool_args else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 171 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 172 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 173 | let Some(cmd) = tool_input |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 174 | .get("command") |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 175 | .and_then(Value::as_str) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 176 | .map(str::to_owned) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 177 | else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 178 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 179 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 180 | let cwd = input.cwd.unwrap_or_else(|| "/".into()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 181 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 182 | match (scan(cfg, &cmd), cfg.enforce.unwrapped) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 183 | (Scan::Clean, _) | (_, Unwrapped::Allow) => {} |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 184 | (Scan::Unwrapped(found), Unwrapped::Deny) => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 185 | let first: Vec<String> = found[0].split_whitespace().map(Into::into).collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 186 | let hint = guess(cfg, &cwd, &first); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 187 | let reason = format!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 188 | "builds go through roundabout here so parallel agents don't oom the box. wrap each build \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 189 | in `rabt run <size> -- <cmd>`, e.g. `rabt run {hint} -- {}`. sizes are {}, or \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 190 | give --ram/--disk/--cores/--time yourself. for anything long use \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 191 | run_in_background and start `rabt watch <lease>` in the background too so you \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 192 | hear if it drags. see the roundabout skill", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 193 | found[0], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 194 | cfg.sizes.keys().cloned().collect::<Vec<_>>().join("/"), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 195 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 196 | emit(&json!({ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 197 | "hookSpecificOutput": { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 198 | "hookEventName": "PreToolUse", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 199 | "permissionDecision": "deny", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 200 | "permissionDecisionReason": reason, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 201 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 202 | })); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 203 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 204 | (Scan::Unwrapped(_), Unwrapped::Wrap) => { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 205 | let hint = guess(cfg, &cwd, std::slice::from_ref(&cmd)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 206 | tool_input["command"] = |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 207 | Value::String(format!("rabt run {hint} -- bash -c {}", shell_quote(&cmd))); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 208 | // no permissionDecision on purpose. allow would skip the user's |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 209 | // prompt for whatever the agent wrote, rewriting shouldn't |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 210 | emit(&json!({ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 211 | "hookSpecificOutput": { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 212 | "hookEventName": "PreToolUse", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 213 | "updatedInput": tool_input, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 214 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 215 | })); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 216 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 217 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 218 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 219 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 220 | fn overdue(session: &str) -> Vec<LeaseInfo> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 221 | if session.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 222 | return Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 223 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 224 | let Some(mut s) = client::connect() else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 225 | return Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 226 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 227 | if proto::send( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 228 | &mut s, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 229 | &Request::Overdue { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 230 | session: session.to_owned(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 231 | }, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 232 | ) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 233 | .is_err() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 234 | { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 235 | return Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 236 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 237 | match Reader::new(&s).and_then(|mut r| r.recv::<Event>()) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 238 | Ok(Some(Event::Overdue { leases })) => leases, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 239 | _ => Vec::new(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 240 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 241 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 242 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 243 | /// same source `rabt run` reads. after `--continue` the env var can keep the |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 244 | /// startup id while the json moves on, and we need both ends to agree |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 245 | fn session(input: &Input) -> String { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 246 | std::env::var("CLAUDE_CODE_SESSION_ID") |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 247 | .ok() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 248 | .filter(|s| !s.is_empty()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 249 | .unwrap_or_else(|| input.session_id.clone()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 250 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 251 | |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 252 | /// both ids, since around /clear the env var and the json can disagree about |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 253 | /// which session just ended. never spawns, a missing daemon holds no intents |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 254 | fn session_end(input: &Input) { |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 255 | let mut ids = vec![session(input), input.session_id.clone()]; |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 256 | ids.dedup(); |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 257 | for session in ids.into_iter().filter(|s| !s.is_empty()) { |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 258 | if let Some(mut s) = client::connect() { |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 259 | let _ = proto::send( |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 260 | &mut s, |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 261 | &Request::Expect { |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 262 | session, |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 263 | need: None, |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 264 | }, |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 265 | ); |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 266 | let _ = Reader::new(&s).and_then(|mut r| r.recv::<Event>()); |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 267 | } |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 268 | } |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 269 | } |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 270 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 271 | fn remind(event: &str, input: &Input) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 272 | let late = overdue(&session(input)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 273 | if late.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 274 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 275 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 276 | let lines: Vec<String> = late |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 277 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 278 | .map(|l| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 279 | format!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 280 | "roundabout lease {} (`{}`) has run {} against a {} estimate. `rabt kill {}` to stop it, \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 281 | `rabt extend {} <dur>` if it's worth the wait", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 282 | l.id, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 283 | l.cmd.join(" "), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 284 | Dur::secs(l.elapsed), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 285 | Dur(l.need.time), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 286 | l.id, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 287 | l.id |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 288 | ) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 289 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 290 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 291 | emit(&json!({ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 292 | "hookSpecificOutput": { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 293 | "hookEventName": event, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 294 | "additionalContext": lines.join("\n"), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 295 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 296 | })); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 297 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 298 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 299 | fn stop(cfg: &Config, event: &str, input: &Input) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 300 | if input.stop_hook_active || cfg.artifacts.stop == StopPolicy::Off { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 301 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 302 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 303 | let root = input.cwd.as_deref().map(paths::project_root); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 304 | let report = artifacts::check(cfg, root.as_deref(), false); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 305 | if !report.over() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 306 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 307 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 308 | let msg = format!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 309 | "{}. run `rabt artifacts gc --cap`, or `rabt artifacts list` and clear out what you \ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 310 | don't need", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 311 | report.describe(cfg) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 312 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 313 | let out = match cfg.artifacts.stop { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 314 | StopPolicy::Block => json!({ "decision": "block", "reason": msg }), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 315 | StopPolicy::Nudge => json!({ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 316 | "hookSpecificOutput": { "hookEventName": event, "additionalContext": msg } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 317 | }), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 318 | StopPolicy::Warn => json!({ "systemMessage": msg }), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 319 | StopPolicy::Off => return, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 320 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 321 | emit(&out); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 322 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 323 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 324 | /// hooks never fail loudly. a broken roundabout shouldn't wedge the agent |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 325 | pub fn main(cfg: &Config, event: &str) -> i32 { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 326 | let Some(input) = read_input() else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 327 | return 0; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 328 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 329 | match event { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 330 | "pre-tool-use" => pre_tool_use(cfg, input), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 331 | "post-tool-use" => remind("PostToolUse", &input), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 332 | "prompt-submit" => remind("UserPromptSubmit", &input), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 333 | "stop" => stop(cfg, "Stop", &input), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 334 | "subagent-stop" => stop(cfg, "SubagentStop", &input), |
ecc1cf8df0f5 | atagen | 2026-09-27 13:36 UTC | 335 | "session-end" => session_end(&input), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 336 | other => eprintln!("rabt: unknown hook {other}"), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 337 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 338 | 0 |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 339 | } |