Synced from monorepo

Synced from monorepo

Changes:
- Release a shell session's resources in one drop
- Make the tools blocking-wait cap client-configurable and self-describing
- Recognize API "exceeds budget" errors as context overflow
- Retry /btw on model overload
- Carry running background tasks and subagents across compaction
- Require round-trip time for SDK liveness checks
- Background-subagent completion reminders with a selectable delivery surface
- Make a PTY shell reap itself until it reaches the registry
- Recover the OS error code from a TLS-phase connection reset
- Consume the attached-client signal and report why idle is withheld
- Treat `.grok/sandbox.toml` edits as protected so auto mode prompts before writing
- Surface history/search in the Ctrl+. cheatsheet and keep it working in history view
- Delete sessions from the dashboard and welcome list
- Release a session's activity record when the session ends
- Stop charging auth-retry budget for fail-closed 401s; reset it across suspends
- Scope skills watches on project vendor roots
- Make [stop] cancel in-flight compaction
- Make the leader soak measure the leader, not its harness

Source-Revision: 8d69c91f02bcacf01e98d5aebbf2f92547c45738
This commit is contained in:
grokkybara[bot] 2026-07-31 18:08:03 +00:00
commit a422116582
165 changed files with 15161 additions and 1969 deletions

View file

@ -5123,7 +5123,11 @@ mod tests {
let local = tokio::task::LocalSet::new();
local
.run_until(async {
for path in ["/etc/hosts", "/home/user/.grok/hooks/evil.json"] {
for path in [
"/etc/hosts",
"/home/user/.grok/hooks/evil.json",
"/home/user/.grok/sandbox.toml",
] {
let mut auto = crate::permission::types::PermissionConfig::new(vec![]);
auto.prompt_policy = PromptPolicy::Auto;
let allow =

View file

@ -328,6 +328,7 @@ pub enum ProtectedEditReason {
StartupFile,
Etc,
GrokConfig,
GrokSandbox,
ClaudeSettings,
CursorHooks,
/// Fail-closed / unclassified sensitive path; no user copy yet.
@ -343,6 +344,7 @@ impl ProtectedEditReason {
Self::StartupFile => "startup_file",
Self::Etc => "etc",
Self::GrokConfig => "grok_config",
Self::GrokSandbox => "grok_sandbox",
Self::ClaudeSettings => "claude_settings",
Self::CursorHooks => "cursor_hooks",
Self::Sensitive => "sensitive",
@ -369,6 +371,9 @@ impl ProtectedEditReason {
Self::GrokConfig => Some(
"Note: This edit contains changes to Grok config, which can alter permissions, tools, and other behavior in later sessions.",
),
Self::GrokSandbox => Some(
"Note: This edit contains changes to the Grok sandbox config, which can loosen filesystem and network restrictions on commands.",
),
Self::ClaudeSettings => Some(
"Note: This edit contains changes to Claude-compatible settings, which can install hooks or change permission mode without a separate execution approval.",
),
@ -471,8 +476,8 @@ fn protected_edit_reason(path: &Path) -> Option<ProtectedEditReason> {
if STARTUP_FILES.contains(&file) {
return Some(ProtectedEditReason::StartupFile);
}
if string_components.ends_with(&[".grok", "config.toml"]) {
return Some(ProtectedEditReason::GrokConfig);
if let Some(reason) = protected_grok_config_file(path, &string_components) {
return Some(reason);
}
if path == Path::new("/etc") || path.starts_with(Path::new("/etc")) {
return Some(ProtectedEditReason::Etc);
@ -480,6 +485,52 @@ fn protected_edit_reason(path: &Path) -> Option<ProtectedEditReason> {
None
}
/// Grok config files that alter permissions (`config.toml`, the
/// `managed_config.toml` defaults tier, the user `requirements.toml` layer) or
/// sandbox restrictions (`sandbox.toml`) in the running and later sessions; a
/// silent edit would let the agent loosen its own guardrails. Matched directly
/// inside any `.grok` dir (user-global default and workspace overlays) and
/// directly under a custom `$GROK_HOME`, which the component match cannot see.
fn protected_grok_config_file(path: &Path, components: &[&str]) -> Option<ProtectedEditReason> {
protected_grok_config_file_with_home(
path,
components,
xai_grok_config::user_grok_home().as_deref(),
)
}
fn protected_grok_config_file_with_home(
path: &Path,
components: &[&str],
user_grok_home: Option<&Path>,
) -> Option<ProtectedEditReason> {
let reason = match components.last().copied() {
Some(
xai_grok_config::USER_CONFIG_FILENAME
| xai_grok_config::MANAGED_CONFIG_FILENAME
| xai_grok_config::REQUIREMENTS_FILENAME,
) => ProtectedEditReason::GrokConfig,
Some("sandbox.toml") => ProtectedEditReason::GrokSandbox,
_ => return None,
};
let in_dot_grok = components.len() >= 2 && components[components.len() - 2] == ".grok";
let in_grok_home = || grok_home_matches(user_grok_home, |home| path.parent() == Some(home));
(in_dot_grok || in_grok_home()).then_some(reason)
}
/// True when `pred` holds for the user grok home in either its lexical or
/// physically-resolved form. Both forms are checked because callers hold a
/// lexical and a resolved candidate path, and the home itself may sit behind a
/// symlink. The comparison is byte-exact (no case folding), like every other
/// resolved-path check in this module.
fn grok_home_matches(home: Option<&Path>, pred: impl Fn(&Path) -> bool) -> bool {
home.is_some_and(|home| {
let lexical = xai_grok_paths::normalize_lexically(home);
pred(&lexical)
|| resolve_following_symlinks(&lexical, 0).is_some_and(|resolved| pred(&resolved))
})
}
fn path_is_under_user_grok_hook_root(path: &Path, grok_home: &Path) -> bool {
path.starts_with(grok_home.join("hooks")) || path == grok_home.join("hooks-paths")
}
@ -487,12 +538,8 @@ fn path_is_under_user_grok_hook_root(path: &Path, grok_home: &Path) -> bool {
fn protected_grok_hook_root(path: &Path, components: &[&str]) -> bool {
components.windows(2).any(|pair| pair == [".grok", "hooks"])
|| components.ends_with(&[".grok", "hooks-paths"])
|| xai_grok_config::user_grok_home().is_some_and(|grok_home| {
let lexical_home = xai_grok_paths::normalize_lexically(&grok_home);
path_is_under_user_grok_hook_root(path, &lexical_home)
|| resolve_following_symlinks(&lexical_home, 0).is_some_and(|resolved_home| {
path_is_under_user_grok_hook_root(path, &resolved_home)
})
|| grok_home_matches(xai_grok_config::user_grok_home().as_deref(), |home| {
path_is_under_user_grok_hook_root(path, home)
})
}
@ -1369,6 +1416,8 @@ mod tests {
"/etc",
"/etc/grok-test",
"/work/subdir/../.git/hooks/pre-commit",
"/home/user/.grok/sandbox.toml",
"/work/project/.grok/sandbox.toml",
] {
assert!(
edit_target_protection(Path::new(path)).is_some(),
@ -1378,6 +1427,9 @@ mod tests {
for path in [
"/work/src/main.rs",
"/work/project/.grok/config.toml/backup",
"/work/project/sandbox.toml",
"/work/project/requirements.toml",
"/work/project/managed_config.toml",
] {
assert!(
edit_target_protection(Path::new(path)).is_none(),
@ -1428,6 +1480,22 @@ mod tests {
"/home/user/.grok/config.toml",
ProtectedEditReason::GrokConfig,
),
(
"/home/user/.grok/sandbox.toml",
ProtectedEditReason::GrokSandbox,
),
(
"/work/project/.grok/sandbox.toml",
ProtectedEditReason::GrokSandbox,
),
(
"/home/user/.grok/managed_config.toml",
ProtectedEditReason::GrokConfig,
),
(
"/home/user/.grok/requirements.toml",
ProtectedEditReason::GrokConfig,
),
(
"/home/user/.claude/settings.json",
ProtectedEditReason::ClaudeSettings,
@ -1551,6 +1619,85 @@ mod tests {
}
}
/// A custom `$GROK_HOME` has no `.grok` path component, so the live
/// `config.toml` / `sandbox.toml` must be caught by the home-prefix branch.
#[test]
fn grok_config_files_under_custom_grok_home_are_protected() {
let home = tempfile::tempdir().unwrap();
let home_path = home.path();
for (file, reason) in [
("config.toml", ProtectedEditReason::GrokConfig),
("managed_config.toml", ProtectedEditReason::GrokConfig),
("requirements.toml", ProtectedEditReason::GrokConfig),
("sandbox.toml", ProtectedEditReason::GrokSandbox),
] {
let path = home_path.join(file);
let components = [file];
assert_eq!(
protected_grok_config_file_with_home(&path, &components, Some(home_path)),
Some(reason),
"{file} directly under $GROK_HOME must be protected"
);
}
// Same file names elsewhere (or with no resolvable home) stay ordinary.
let elsewhere = home_path.join("sub").join("sandbox.toml");
assert_eq!(
protected_grok_config_file_with_home(
&elsewhere,
&["sub", "sandbox.toml"],
Some(home_path)
),
None
);
assert_eq!(
protected_grok_config_file_with_home(
&home_path.join("sandbox.toml"),
&["sandbox.toml"],
None
),
None
);
}
/// The resolved-symlink arm of the grok-home match must decide: `$GROK_HOME`
/// points at a symlink while the edit targets the physical home directory,
/// so the lexical parent-equality arm cannot fire.
#[test]
#[cfg(unix)]
fn grok_config_under_symlinked_grok_home_is_protected() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let real_home = tmp.path().join("real-home");
std::fs::create_dir(&real_home).unwrap();
let link = tmp.path().join("home-link");
symlink(&real_home, &link).unwrap();
// tempdir paths can themselves contain symlinks (macOS /var -> /private/var);
// compare against the physical home the production resolver will produce.
let physical_home = resolve_following_symlinks(&real_home, 0).unwrap();
assert_eq!(
protected_grok_config_file_with_home(
&physical_home.join("sandbox.toml"),
&["sandbox.toml"],
Some(&link)
),
Some(ProtectedEditReason::GrokSandbox)
);
}
/// `protected_edit_reason` lowercases path components before matching, so
/// the canonical filename constants must stay lowercase or the const
/// patterns silently stop firing.
#[test]
fn protected_config_filename_constants_are_lowercase() {
for name in [
xai_grok_config::USER_CONFIG_FILENAME,
xai_grok_config::MANAGED_CONFIG_FILENAME,
xai_grok_config::REQUIREMENTS_FILENAME,
] {
assert_eq!(name, name.to_ascii_lowercase(), "{name}");
}
}
#[test]
fn resolved_root_alias_matches_physical_destination() {
let resolved_root = resolve_following_symlinks(Path::new("/etc"), 0).unwrap();