Synced from monorepo
Synced from monorepo Changes: - Workspace server: report `/ready` as failed with dwell on hub connect failure - Refresh OIDC token for the Grok agent in the shell - ACP terminal output recorder - Cross-platform provider auth commands in the shell - Default `/resume` to Grok sessions with a hint for hidden external sessions - Resume sessions by title with `--resume` - Limit app-builder archive size - Data-driven tag labels for slash commands - Doctor fixes for tmux - Custom provider gateways and subprocess environment policy in the shell - `/tutorial` — opt-in onboarding tour of Grok Build - Soft and required CLI version checks in the shell - Privacy banner env overrides survive live settings updates - Add remote flag to override the image-edit model - Return profile fields from auth info even when the access token is expired - Add edit control on queued prompt rows - Keep fail-closed policy when clearing orphans with no team - Setting to disable the Ctrl+Space/F8 voice shortcut - Pass `--raw` to pw-record so Linux dictation works on older PipeWire - Validate git URLs when adding marketplace entries - Stop shipping stale tool-doc parameter and tool names - Re-point dashboard attach after `/fork` only when the parent was attached - Surface Grok Computer media-generation results as file-path chunks - Clear web background-task tray on kill and keep the task description - Show privacy upsell banner in agent view until acted on - Add tools-server client callback surface - Protect persistent global hook sources Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
This commit is contained in:
parent
a5727c5960
commit
69f0ba880a
286 changed files with 22939 additions and 9624 deletions
|
|
@ -11,13 +11,15 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
use crate::deny::{
|
||||
apply_deny_globs_to_capability_set, apply_deny_paths_to_capability_set, effective_deny_paths,
|
||||
partition_deny_entries,
|
||||
apply_deny_globs_to_capability_set, apply_deny_paths_to_capability_set,
|
||||
apply_write_deny_paths_to_capability_set, effective_deny_paths, partition_deny_entries,
|
||||
};
|
||||
use crate::hook_write_deny::profile_hook_write_deny;
|
||||
use crate::paths::grok_home;
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
use crate::paths::{DEVICE_DIRS, DEVICE_FILES};
|
||||
use crate::paths::{essential_writable_paths, essential_writable_paths_minimal};
|
||||
use xai_grok_config::GlobalHookSource;
|
||||
|
||||
/// A resolved sandbox profile ready to be converted to a `CapabilitySet`.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -30,12 +32,18 @@ pub struct SandboxProfile {
|
|||
pub read_write: Vec<PathBuf>,
|
||||
/// Paths denied entirely (overrides read_only/read_write)
|
||||
pub deny: Vec<PathBuf>,
|
||||
/// Typed direct global hook sources (write-denied, still readable).
|
||||
pub write_deny: Vec<GlobalHookSource>,
|
||||
/// Whether to grant read access to the entire filesystem by default
|
||||
pub default_read: bool,
|
||||
/// Whether child processes should have network blocked
|
||||
pub restrict_network: bool,
|
||||
}
|
||||
|
||||
fn resolve_write_deny(profile: &ProfileName) -> anyhow::Result<Vec<GlobalHookSource>> {
|
||||
profile_hook_write_deny(profile)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ProfileConfig {
|
||||
#[serde(default)]
|
||||
|
|
@ -280,6 +288,27 @@ impl ProfileName {
|
|||
}
|
||||
}
|
||||
|
||||
// Direct global-hook write-deny (macOS Seatbelt; Linux via bwrap).
|
||||
if !profile.write_deny.is_empty() {
|
||||
let mut pairs: Vec<(PathBuf, bool)> = profile
|
||||
.write_deny
|
||||
.iter()
|
||||
.map(|s| (s.path.clone(), s.is_dir()))
|
||||
.collect();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let files =
|
||||
xai_grok_config::validated_hook_json_files_for_sources(&profile.write_deny)
|
||||
.map_err(|e| anyhow::anyhow!("hook JSON alias validation failed: {e}"))?;
|
||||
for f in files {
|
||||
if !pairs.iter().any(|(p, _)| p == &f) {
|
||||
pairs.push((f, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
apply_write_deny_paths_to_capability_set(&mut caps, &pairs, &profile.read_write)?;
|
||||
}
|
||||
|
||||
// Kernel deny (read+write): macOS Seatbelt rules; Linux via bwrap bind-over.
|
||||
// The effective deny set is the profile's own `deny` (custom profiles only;
|
||||
// built-ins carry an empty `deny`). An empty set means there is nothing to
|
||||
|
|
@ -325,6 +354,7 @@ impl ProfileName {
|
|||
read_only: vec![],
|
||||
read_write: essential_writable_paths(workspace),
|
||||
deny: vec![],
|
||||
write_deny: resolve_write_deny(self)?,
|
||||
default_read: true,
|
||||
restrict_network: false,
|
||||
}),
|
||||
|
|
@ -363,6 +393,7 @@ impl ProfileName {
|
|||
read_only: vec![],
|
||||
read_write,
|
||||
deny: vec![],
|
||||
write_deny: vec![],
|
||||
default_read: true,
|
||||
restrict_network: false,
|
||||
})
|
||||
|
|
@ -373,6 +404,7 @@ impl ProfileName {
|
|||
read_only: vec![],
|
||||
read_write: essential_writable_paths_minimal(),
|
||||
deny: vec![],
|
||||
write_deny: resolve_write_deny(self)?,
|
||||
default_read: true,
|
||||
restrict_network: true,
|
||||
}),
|
||||
|
|
@ -398,6 +430,7 @@ impl ProfileName {
|
|||
.chain(std::iter::once(home.join("Library")))
|
||||
.filter(|p| p.exists())
|
||||
.chain(std::iter::once(workspace.to_path_buf()))
|
||||
.chain(std::iter::once(grok_home()))
|
||||
.collect();
|
||||
|
||||
Ok(SandboxProfile {
|
||||
|
|
@ -405,6 +438,7 @@ impl ProfileName {
|
|||
read_only: system_read,
|
||||
read_write: essential_writable_paths(workspace),
|
||||
deny: vec![],
|
||||
write_deny: resolve_write_deny(self)?,
|
||||
default_read: false,
|
||||
restrict_network: true,
|
||||
})
|
||||
|
|
@ -422,7 +456,7 @@ impl ProfileName {
|
|||
})?;
|
||||
|
||||
// Start from the base profile if `extends` is set
|
||||
let mut profile = if let Some(base_name) = &profile_config.extends {
|
||||
let (base, mut profile) = if let Some(base_name) = &profile_config.extends {
|
||||
let base: ProfileName = base_name.parse().map_err(|e: String| {
|
||||
anyhow::anyhow!("Profile '{name}' extends invalid base: {e}")
|
||||
})?;
|
||||
|
|
@ -438,10 +472,10 @@ impl ProfileName {
|
|||
cannot extend other custom profiles (only built-ins)"
|
||||
);
|
||||
}
|
||||
base.resolve(workspace, config)?
|
||||
let resolved = base.resolve(workspace, config)?;
|
||||
(base, resolved)
|
||||
} else {
|
||||
// Default: start from workspace
|
||||
Self::Workspace.resolve(workspace, config)?
|
||||
(Self::Workspace, Self::Workspace.resolve(workspace, config)?)
|
||||
};
|
||||
|
||||
profile.name = name.clone();
|
||||
|
|
@ -466,6 +500,10 @@ impl ProfileName {
|
|||
profile.deny.push(PathBuf::from(path_str));
|
||||
}
|
||||
|
||||
if matches!(base, Self::Devbox) {
|
||||
profile.write_deny.clear();
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
}
|
||||
|
|
@ -528,8 +566,26 @@ mod tests {
|
|||
assert_eq!(p.to_string(), "my-custom");
|
||||
}
|
||||
|
||||
/// Hosts with a retargetable `$GROK_HOME/hooks` symlink (fail-closed under
|
||||
/// write-deny) cannot resolve enforcing profiles against the real home.
|
||||
fn skip_if_host_hook_write_deny_unresolvable() -> bool {
|
||||
if !crate::hook_write_deny::profile_enforces_hook_write_deny(&ProfileName::Workspace) {
|
||||
return false;
|
||||
}
|
||||
match crate::hook_write_deny::resolve_hook_write_deny_snapshot() {
|
||||
Ok(_) => false,
|
||||
Err(e) => {
|
||||
eprintln!("skipping profile resolve test: host hook write-deny unresolvable ({e})");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_in_network_restriction_values() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let config = SandboxConfig::default();
|
||||
|
||||
|
|
@ -593,6 +649,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn custom_network_restriction_inherits_and_overrides_base() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let config = network_inheritance_config();
|
||||
|
||||
|
|
@ -611,6 +670,9 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn strict_allowlist_includes_run_and_var_when_present() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
// Regression: /run (resolv realpath) + /var (NSS/SSSD) when present.
|
||||
let workspace = std::env::temp_dir();
|
||||
let profile = ProfileName::Strict
|
||||
|
|
@ -636,6 +698,9 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn base_profile_capability_set_builds() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
// A base profile with no `deny` builds a CapabilitySet without erroring.
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let config = SandboxConfig::default();
|
||||
|
|
@ -646,6 +711,9 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn custom_profile_from_config() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let config = SandboxConfig {
|
||||
profiles: HashMap::from([(
|
||||
|
|
@ -901,6 +969,9 @@ read_write = ["/tmp/ci-artifacts"]
|
|||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn strict_capability_set_builds_without_openable_dev_tty() {
|
||||
if skip_if_host_hook_write_deny_unresolvable() {
|
||||
return;
|
||||
}
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let result = ProfileName::Strict.to_capability_set(&workspace);
|
||||
assert!(
|
||||
|
|
|
|||
Loading…
Reference in a new issue