Synced from monorepo

Synced from monorepo

Changes:
- Shell: accept target response id on rewind execute
- Shell: stamp response id on chat user message chunks
- Worktree: optional rebuild and stale git registration cleanup in auto-GC
- Worktree: kind-aware auto-GC TTLs and config knobs
- Worktree: macOS process CWD scan and Unix PID liveness for GC guards
- Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only)
- Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups
- Shell: stop overwriting user skills
- Tools: read markdown in `skills/` directories untruncated
- `/usage` shows per-session token and dollar usage in the TUI
- Security: prompt on environment-dumping `ps` variants
- Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission
- Tools: make scheduler deletion durable
- Shell: add relocation storage primitives
- Shell: give side model calls their own conversation ids
- Fix five workflow-runtime bugs (budget, pause, cancel, reconnect)
- Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask)
- Pager: expose doctor in the TUI
- Security: block unauthorized RCE via abused safe commands
- Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent"
- Security: block `rg --pre` arbitrary code execution in auto-mode
- Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section
- App builder deployer: `allow_forking` and `show_built_with_grok`
- Pager: stop stacking duplicate "Worked for" markers on parked turns
- Shell: support `max` as a distinct reasoning effort tier
- Tools: serialize background `/loop` fires on the whole work unit
- Shell: add working-directory relocation state primitives
- Proto: `ClientToolResult` and `ChatConfig` client-side tools
- Shell: model providers
- Chat: select App Builder product on the Build path
- Shell: attach author identity to feedback when the deployment opts in
- Doctor: fix for SSH wrap setup
- Workflow authoring skills: create-workflow and import-claude-workflow docs
- Add read-only grok doctor
- Sandbox: apply Landlock without a controlling TTY
- Pager: recover image paste over grok wrap on headless remotes
- Pager: make actions screen-mode aware
- Shell: resume sessions when the working directory moves
- Pager: centralize terminal diagnostics
- Workspace: gate inline shell file access
- Pager: centralize terminal probes
- Pager: edit minimal prompts in an external editor
- Pager: standardize backgrounding on Ctrl+B
- Shell: recap rides the parent turn's prompt cache
- Tools: add scheduler lifecycle version clock

Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
This commit is contained in:
grokkybara[bot] 2026-07-21 18:10:23 +00:00
commit 3af4d5d398
556 changed files with 56609 additions and 21892 deletions

View file

@ -56,12 +56,13 @@ xai-test-utils = { workspace = true }
# Only used by tests behind #[cfg(feature = "metadata")] in db/tests.rs.
rusqlite = { version = "0.37", features = ["bundled"] }
[target.'cfg(target_os = "linux")'.dev-dependencies]
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[target.'cfg(target_os = "linux")'.dev-dependencies]
nix = { version = "0.30", features = ["fs"] }
[target.'cfg(target_os = "linux")'.dependencies]
libc = { workspace = true }
nix = { version = "0.30", features = ["fs"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use xai_sqlite_journal::JournalMode;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WorktreeKind {
Session,
@ -38,14 +38,33 @@ impl WorktreeKind {
}
pub fn from_str_lossy(s: &str) -> Self {
Self::from_str_exact(s).unwrap_or(Self::Manual)
}
/// Exact known kind key. Unknown → None (unlike [`Self::from_str_lossy`]).
pub fn from_str_exact(s: &str) -> Option<Self> {
match s {
"session" => Self::Session,
"ab" => Self::Ab,
"pool" => Self::Pool,
"fork" => Self::Fork,
"manual" => Self::Manual,
"subagent" => Self::Subagent,
_ => Self::Manual,
"session" => Some(Self::Session),
"ab" => Some(Self::Ab),
"pool" => Some(Self::Pool),
"fork" => Some(Self::Fork),
"manual" => Some(Self::Manual),
"subagent" => Some(Self::Subagent),
_ => None,
}
}
/// Config key parse: trim + case-insensitive; unknown → None.
pub fn from_str_opt(s: &str) -> Option<Self> {
let t = s.trim();
if let Some(k) = Self::from_str_exact(t) {
return Some(k);
}
// Only allocate lowercase when needed.
if t.bytes().any(|b| b.is_ascii_uppercase()) {
Self::from_str_exact(&t.to_ascii_lowercase())
} else {
None
}
}
}
@ -306,6 +325,35 @@ impl WorktreeDb {
pub fn sweep_dead(&self) -> Result<u64> {
queries::sweep_dead(&self.conn)
}
/// Read a value from the `meta` table. `Ok(None)` when the key is absent.
pub fn get_meta(&self, key: &str) -> Result<Option<String>> {
match self
.conn
.query_row(schema::GET_META, [key], |row| row.get(0))
{
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e).with_context(|| format!("failed to read meta key {key}")),
}
}
/// Insert or replace a `meta` table value.
pub fn set_meta(&self, key: &str, value: &str) -> Result<()> {
self.conn
.execute(schema::UPSERT_META, rusqlite::params![key, value])
.with_context(|| format!("failed to write meta key {key}"))?;
Ok(())
}
/// Test-only: run raw SQL (e.g. drop tables to force fail-closed paths).
#[cfg(test)]
pub(crate) fn execute_batch_for_test(&self, sql: &str) -> Result<()> {
self.conn
.execute_batch(sql)
.context("execute_batch_for_test failed")?;
Ok(())
}
}
/// Derive a worktree ID from its destination path: `<basename>-<hash of full path>`

View file

@ -154,6 +154,21 @@ pub struct RebuildReport {
pub already_tracked: u64,
}
fn managed_worktree_roots(grok_home: &Path) -> [PathBuf; 2] {
[grok_home.join("worktrees"), grok_home.join("worktree_pool")]
.map(|root| dunce::canonicalize(&root).unwrap_or(root))
}
/// True when `path` is under a managed root (`worktrees/` or `worktree_pool/`).
/// Prefer already-canonical `path`; roots are canonicalized inside.
pub fn path_under_managed_worktree_roots(path: &Path, grok_home: &Path) -> bool {
path_under_roots(path, &managed_worktree_roots(grok_home))
}
fn path_under_roots(path: &Path, roots: &[PathBuf]) -> bool {
roots.iter().any(|root| path.starts_with(root))
}
pub fn rebuild_worktree_db(
db: &crate::db::WorktreeDb,
grok_home: &Path,
@ -163,16 +178,29 @@ pub fn rebuild_worktree_db(
discovered: discovery.found.len() as u64,
..Default::default()
};
let now = now_epoch_secs();
let roots = managed_worktree_roots(grok_home);
for wt in discovery.found {
let path = dunce::canonicalize(&wt.path).unwrap_or_else(|_| wt.path.clone());
// Refuse symlink escape outside managed roots.
if !path_under_roots(&path, &roots) {
tracing::warn!(
path = %path.display(),
"rebuild skipped path outside grok worktrees/worktree_pool"
);
continue;
}
let id = id_from_path(&path);
let path_str = path.to_string_lossy();
if db.get_by_id(&id)?.is_some() || db.get(&path_str)?.is_some() {
report.already_tracked += 1;
continue;
}
db.register(&wt.into_record())?;
let mut rec = wt.into_record();
// Touch so same-pass age GC does not reclaim solely from old FS mtime.
rec.last_accessed_at = Some(now);
db.register(&rec)?;
report.registered += 1;
}
@ -327,4 +355,45 @@ mod tests {
assert_eq!(deser.registered, 3);
assert_eq!(deser.already_tracked, 2);
}
#[test]
fn rebuild_sets_last_accessed_at() {
let tmp = tempfile::TempDir::new().unwrap();
let grok_home = tmp.path();
let wt = grok_home.join("worktrees/repo/sess");
make_fake_standalone_worktree(&wt);
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
rebuild_worktree_db(&db, grok_home).unwrap();
let rec = db.get(&wt.to_string_lossy()).unwrap().expect("registered");
assert!(
rec.last_accessed_at.is_some(),
"rebuild must touch last_accessed_at for same-pass age safety"
);
}
#[cfg(unix)]
#[test]
fn rebuild_skips_symlink_escape_outside_managed_roots() {
let tmp = tempfile::TempDir::new().unwrap();
let grok_home = tmp.path().join("grok");
let outside = tmp.path().join("outside-real");
make_fake_standalone_worktree(&outside);
let link_parent = grok_home.join("worktrees/repo");
std::fs::create_dir_all(&link_parent).unwrap();
std::os::unix::fs::symlink(&outside, link_parent.join("escaped")).unwrap();
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
let report = rebuild_worktree_db(&db, &grok_home).unwrap();
assert_eq!(report.discovered, 1);
assert_eq!(report.registered, 0, "symlink escape must not register");
assert!(
db.list(&crate::db::ListFilter::default())
.unwrap()
.is_empty()
);
assert!(!path_under_managed_worktree_roots(
&dunce::canonicalize(&outside).unwrap(),
&grok_home
));
}
}

View file

@ -1207,7 +1207,9 @@ mod tests {
rehydrate_worktree_from_ref(&dest, &repo_path, &snap, Some("subagent-42")).unwrap();
// Filter to OUR record by path: concurrent open_default writers may add
// other subagent rows since GROK_HOME is process-global.
// other subagent rows since GROK_HOME is process-global. Match the
// canonical path register_worktree stores (/var → /private/var on macOS).
let dest_canon = dunce::canonicalize(&dest).unwrap_or_else(|_| dest.clone());
let db = crate::db::WorktreeDb::open(&fx.home).unwrap();
let mine: Vec<_> = db
.list(&crate::db::ListFilter {
@ -1216,7 +1218,7 @@ mod tests {
})
.unwrap()
.into_iter()
.filter(|r| r.path == dest)
.filter(|r| r.path == dest || r.path == dest_canon)
.collect();
assert_eq!(mine.len(), 1, "exactly one rehydrated subagent record");
assert_eq!(mine[0].kind, crate::db::WorktreeKind::Subagent);

View file

@ -9,6 +9,8 @@
//! 6. SQLite metadata tracking (behind `metadata` feature)
mod api;
#[cfg(feature = "metadata")]
mod auto_gc;
#[cfg(target_os = "linux")]
pub mod btrfs;
mod copy;
@ -31,6 +33,8 @@ pub use api::cleanup_orphaned_btrfs_snapshots;
#[cfg(target_os = "linux")]
pub use api::cleanup_orphaned_overlay_snapshots;
#[cfg(feature = "metadata")]
pub use api::gc::effective_max_age;
#[cfg(feature = "metadata")]
pub use api::gc::{GcOptions, GcReport, gc_worktrees, gc_worktrees_with_delegate};
pub use api::{
BtrfsDelegate, BtrfsMode, CleanupReport, CopyReport, CreationMode, DelegateSnapshotResult,
@ -39,6 +43,17 @@ pub use api::{
cleanup_worktrees_in_with_delegate, remove_worktree, remove_worktree_with_delegate,
};
#[cfg(feature = "metadata")]
pub use auto_gc::{
AutoGcOptions, AutoGcOutcome, AutoGcReport, DEFAULT_MAX_AGE_SECS, DEFAULT_MIN_INTERVAL_SECS,
DEFAULT_REBUILD_MIN_INTERVAL_SECS, ENV_AUTO_GC, ENV_AUTO_GC_DRY_RUN, ENV_AUTO_GC_MAX_AGE,
ENV_AUTO_GC_REBUILD, MAX_AGE_SECS_MAX, MAX_AGE_SECS_MIN, META_LAST_AUTO_GC_AT,
META_LAST_AUTO_REBUILD_AT, MIN_INTERVAL_SECS_MAX, MIN_INTERVAL_SECS_MIN,
ResolvedWorktreeAutoGc, WorktreeAutoGcLayer, age_expiry_allowed, build_auto_gc_options,
clamp_max_age_secs, clamp_min_interval_secs, default_max_age_by_kind, env_auto_gc_disabled,
env_auto_gc_dry_run, env_auto_gc_max_age, env_auto_gc_rebuild, maybe_auto_gc,
maybe_auto_gc_default, process_cwd_scan_available, resolve_worktree_auto_gc_from_layers,
};
#[cfg(feature = "metadata")]
pub use db::{
DbStats, ListFilter, WorktreeDb, WorktreeKind, WorktreeRecord, WorktreeStatus, id_from_path,
now_epoch_secs, repo_name_from_path, resolve_grok_home,