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
|
|
@ -112,6 +112,112 @@ fn emit_seatbelt_deny(caps: &mut CapabilitySet, filter: &str) -> anyhow::Result<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Emit write-only Seatbelt deny rules (hook sources stay readable).
|
||||
#[cfg(all(feature = "enforce", target_os = "macos"))]
|
||||
fn emit_seatbelt_write_deny(caps: &mut CapabilitySet, filter: &str) -> anyhow::Result<()> {
|
||||
caps.add_platform_rule(format!("(deny file-write* {filter})"))?;
|
||||
for action in SEATBELT_WRITE_DENY_ACTIONS {
|
||||
caps.add_platform_rule(format!("(deny {action} {filter})"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Unlink blocks rename of the node; create blocks replacement. Specific
|
||||
// sub-actions (not bare file-write*) win against later allow-write* grants.
|
||||
#[cfg(all(feature = "enforce", target_os = "macos"))]
|
||||
const SEATBELT_ANCESTOR_NODE_DENY_ACTIONS: &[&str] = &["file-write-unlink", "file-write-create"];
|
||||
|
||||
#[cfg(all(feature = "enforce", target_os = "macos"))]
|
||||
fn emit_seatbelt_ancestor_node_deny(caps: &mut CapabilitySet, filter: &str) -> anyhow::Result<()> {
|
||||
for action in SEATBELT_ANCESTOR_NODE_DENY_ACTIONS {
|
||||
caps.add_platform_rule(format!("(deny {action} {filter})"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leaf parent up to deepest containing writable root; outside all roots → empty.
|
||||
#[cfg(all(feature = "enforce", target_os = "macos"))]
|
||||
pub(crate) fn ancestors_within_writable_roots(
|
||||
path: &Path,
|
||||
writable_roots: &[PathBuf],
|
||||
) -> Vec<PathBuf> {
|
||||
let root = writable_roots
|
||||
.iter()
|
||||
.filter(|r| path == r.as_path() || path.starts_with(r))
|
||||
.max_by_key(|r| r.components().count());
|
||||
let Some(root) = root else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for anc in xai_grok_config::existing_ancestor_chain(path) {
|
||||
if anc == *root || anc.starts_with(root) {
|
||||
out.push(anc);
|
||||
}
|
||||
}
|
||||
if path != root.as_path() && root.exists() && !out.iter().any(|p| p == root) {
|
||||
out.push(root.clone());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Write-only deny for hook sources. Linux is a no-op (bwrap).
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) fn apply_write_deny_paths_to_capability_set(
|
||||
caps: &mut CapabilitySet,
|
||||
entries: &[(PathBuf, bool)],
|
||||
writable_roots: &[PathBuf],
|
||||
) -> anyhow::Result<()> {
|
||||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut rule_paths = Vec::new();
|
||||
let mut ancestor_seen = std::collections::HashSet::new();
|
||||
for (path, is_dir) in entries {
|
||||
let canonical = dunce::canonicalize(path).unwrap_or_else(|_| path.clone());
|
||||
let use_subpath = *is_dir || deny_path_is_dir(&canonical);
|
||||
for form in macos_deny_aliases(path, &canonical) {
|
||||
let Some(escaped) = escape_seatbelt_path(&form) else {
|
||||
anyhow::bail!("cannot escape write-deny path {form:?} for Seatbelt");
|
||||
};
|
||||
if use_subpath {
|
||||
emit_seatbelt_write_deny(caps, &format!("(literal \"{escaped}\")"))?;
|
||||
emit_seatbelt_write_deny(caps, &format!("(subpath \"{escaped}\")"))?;
|
||||
} else {
|
||||
emit_seatbelt_write_deny(caps, &format!("(literal \"{escaped}\")"))?;
|
||||
}
|
||||
rule_paths.push(form);
|
||||
}
|
||||
for anc in ancestors_within_writable_roots(path, writable_roots) {
|
||||
if !ancestor_seen.insert(anc.clone()) {
|
||||
continue;
|
||||
}
|
||||
let anc_canon = dunce::canonicalize(&anc).unwrap_or_else(|_| anc.clone());
|
||||
for form in macos_deny_aliases(&anc, &anc_canon) {
|
||||
let Some(escaped) = escape_seatbelt_path(&form) else {
|
||||
anyhow::bail!(
|
||||
"cannot escape ancestor write-deny path {form:?} for Seatbelt"
|
||||
);
|
||||
};
|
||||
emit_seatbelt_ancestor_node_deny(caps, &format!("(literal \"{escaped}\")"))?;
|
||||
rule_paths.push(form);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = caps.remove_exact_file_caps_for_paths(&rule_paths);
|
||||
tracing::info!(
|
||||
count = entries.len(),
|
||||
"Applied Seatbelt write-deny for Grok-owned direct hook sources"
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = (caps, writable_roots);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply kernel-level deny rules for the given paths.
|
||||
///
|
||||
/// On macOS, adds Seatbelt read-deny + write-deny (incl. specific write
|
||||
|
|
@ -238,6 +344,49 @@ mod tests {
|
|||
#[cfg(all(feature = "enforce", unix))]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(all(feature = "enforce", target_os = "macos"))]
|
||||
fn ancestors_pin_under_writable_root_not_home() {
|
||||
let tmp = std::env::temp_dir().join(format!(
|
||||
"grok-anc-policy-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
let grok = tmp.join("grok");
|
||||
let sessions = grok.join("sessions");
|
||||
let leaf = sessions.join("extra-hooks");
|
||||
std::fs::create_dir_all(&leaf).unwrap();
|
||||
let ws = tmp.join("ws");
|
||||
std::fs::create_dir_all(&ws).unwrap();
|
||||
|
||||
let roots = [grok.clone(), ws.clone()];
|
||||
let pin = ancestors_within_writable_roots(&leaf, &roots);
|
||||
assert!(
|
||||
pin.iter().any(|p| p == &sessions),
|
||||
"must pin sessions under GROK_HOME: {pin:?}"
|
||||
);
|
||||
assert!(
|
||||
pin.iter().any(|p| p == &grok),
|
||||
"must pin GROK_HOME grant root: {pin:?}"
|
||||
);
|
||||
assert!(
|
||||
!pin.iter().any(|p| p == &tmp),
|
||||
"must not pin above writable roots: {pin:?}"
|
||||
);
|
||||
|
||||
let outside = tmp.join("outside").join("hooks");
|
||||
std::fs::create_dir_all(&outside).unwrap();
|
||||
let pin_out = ancestors_within_writable_roots(&outside, &roots);
|
||||
assert!(
|
||||
pin_out.is_empty(),
|
||||
"source outside writable roots: leaf-only: {pin_out:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn resolve_deny_paths_relative() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue