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

@ -22,7 +22,7 @@ pub(crate) fn grok_home() -> PathBuf {
/// or seed RNGs.
///
/// These are individual files (use `allow_file`, not `allow_path`).
/// `/dev/pts` is a directory (PTY slaves on Linux) so it uses `allow_path`.
/// Directory nodes under `/dev` belong in [`DEVICE_DIRS`].
#[cfg(all(feature = "enforce", unix))]
pub(crate) const DEVICE_FILES: &[&str] = &[
"/dev/null", // output sink — used by virtually every CLI tool
@ -31,13 +31,13 @@ pub(crate) const DEVICE_FILES: &[&str] = &[
"/dev/urandom", // entropy — used by crypto/TLS
"/dev/tty", // controlling terminal — used by git, ssh, gpg
"/dev/ptmx", // PTY allocation — used by terminal spawning
"/dev/fd", // file descriptor access (symlink to /proc/self/fd on Linux)
];
/// Device directories that need write access.
/// Device directories that need write access (use `allow_path`, not `allow_file`).
#[cfg(all(feature = "enforce", unix))]
pub(crate) const DEVICE_DIRS: &[&str] = &[
"/dev/pts", // PTY slaves (Linux)
"/dev/fd", // fd table (symlink to /proc/self/fd on Linux; a directory)
];
// ── Temporary directories ───────────────────────────────────────────────────

View file

@ -169,6 +169,31 @@ fn load_config_file(path: &Path) -> Option<SandboxConfig> {
}
}
/// Whether a device **file** entry is safe to pass to `allow_file` / Landlock
/// PathFd materialization.
///
/// `/dev/tty` always exists, but without a controlling terminal `open()` returns
/// ENXIO and nono's apply aborts the **entire** ruleset. Built-in profiles fail
/// open, which was a silent sandbox bypass under `setsid`/CI/headless launches.
///
/// Only that class of failure (and missing nodes) is filtered here. Other open
/// errors — notably **EISDIR** on directory nodes — must not drop the path:
/// directories are granted via [`DEVICE_DIRS`] / `allow_path`, and a plain
/// `File::open` EISDIR does not mean Landlock would reject the grant.
#[cfg(all(feature = "enforce", unix))]
fn device_file_openable(path: &Path) -> bool {
match std::fs::File::open(path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
// ENXIO/ENODEV: e.g. /dev/tty with no controlling terminal — PathFd
// materialization would abort the whole Landlock ruleset.
Err(e) if matches!(e.raw_os_error(), Some(libc::ENXIO) | Some(libc::ENODEV)) => false,
// EISDIR, EACCES, etc.: still attempt the grant path. allow_file may
// reject ExpectedFile; that only skips this entry, not the whole apply.
Err(_) => true,
}
}
impl ProfileName {
/// Convert this profile into a nono `CapabilitySet` for the given workspace.
#[cfg(all(feature = "enforce", unix))]
@ -238,7 +263,9 @@ impl ProfileName {
// Device special files (character devices like /dev/null, /dev/tty, etc.).
for dev in DEVICE_FILES {
let p = Path::new(dev);
if !p.exists() {
// nono opens each entry read-only at apply time, so a node that exists
// but cannot be opened would abort the whole ruleset, not just itself.
if !device_file_openable(p) {
continue;
}
if let Err(e) = caps.allow_file_mut(p, AccessMode::ReadWrite) {
@ -820,4 +847,94 @@ read_write = ["/tmp/ci-artifacts"]
.expect_err("Off.resolve must Err");
assert!(err.to_string().contains("off"), "unexpected error: {err}");
}
#[test]
#[cfg(all(feature = "enforce", unix))]
fn enxio_device_file_is_skipped_but_directory_is_not() {
assert!(
device_file_openable(Path::new("/dev/null")),
"openable device must still be allow-listed"
);
// /dev/tty without a controlling terminal → ENXIO (the apply-abort case).
// Skip the assertion when a ctty is present (open succeeds).
match std::fs::File::open("/dev/tty") {
Err(e) if e.raw_os_error() == Some(libc::ENXIO) => {
assert!(
!device_file_openable(Path::new("/dev/tty")),
"ENXIO /dev/tty must be skipped so Landlock apply cannot abort"
);
}
Ok(_) | Err(_) => {}
}
// Directories must stay grantable. On Linux, File::open returns EISDIR;
// on macOS it often succeeds. Either way the probe must return true so
// directory devices (e.g. /dev/fd via DEVICE_DIRS) are not dropped.
let dir = std::env::temp_dir().join(format!("grok-sbx-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
match std::fs::File::open(&dir) {
Err(e) => {
assert_eq!(
e.raw_os_error(),
Some(libc::EISDIR),
"unexpected directory open error: {e}"
);
assert!(
device_file_openable(&dir),
"EISDIR must not drop a path from grant consideration"
);
}
Ok(_) => {
assert!(
device_file_openable(&dir),
"openable directory must remain grantable"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
/// Building the strict CapabilitySet must succeed even when /dev/tty cannot
/// be opened (no controlling terminal). Regression for the silent Landlock
/// apply-abort under setsid/CI/headless.
#[test]
#[cfg(all(feature = "enforce", unix))]
fn strict_capability_set_builds_without_openable_dev_tty() {
let workspace = std::env::current_dir().unwrap();
let result = ProfileName::Strict.to_capability_set(&workspace);
assert!(
result.is_ok(),
"strict CapabilitySet must build even if /dev/tty is unopenable: {:?}",
result.err()
);
}
/// `/dev/fd` is a directory (→ `/proc/self/fd` on Linux). It must be granted
/// via DEVICE_DIRS/`allow_path`, not dropped by a file-open EISDIR probe.
#[test]
#[cfg(all(feature = "enforce", unix))]
fn dev_fd_is_granted_as_device_dir_not_skipped_as_file() {
assert!(
!DEVICE_FILES.contains(&"/dev/fd"),
"/dev/fd must not sit in DEVICE_FILES (File::open → EISDIR)"
);
assert!(
DEVICE_DIRS.contains(&"/dev/fd"),
"/dev/fd must be in DEVICE_DIRS so allow_path can grant it"
);
let dev_fd = Path::new("/dev/fd");
if dev_fd.exists() {
// Directory open fails with EISDIR for plain File::open — the probe
// must still report grantable so we don't regress directory devices.
assert!(
device_file_openable(dev_fd),
"/dev/fd must not be filtered out by the ENXIO-only open probe"
);
assert!(
dev_fd.is_dir(),
"expected /dev/fd to be a directory on this platform"
);
}
}
}