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:
grokkybara[bot] 2026-07-23 17:12:33 +00:00
commit 69f0ba880a
286 changed files with 22939 additions and 9624 deletions

View file

@ -13,6 +13,7 @@ pub mod path_suggestions;
pub(crate) mod query_tools;
pub mod remap;
pub mod serde_base64;
pub mod shell_env_policy;
pub mod spawn;
pub mod truncate;
pub mod unicode_confusables;
@ -26,6 +27,10 @@ pub use fs::{UnicodePathMatch, canonicalize_with_timeout, try_resolve_unicode_fi
pub use grok_home::{grok_application, grok_home};
pub use path_suggestions::format_not_found_error;
pub use remap::{remap_json_keys, remap_schema_properties, reverse_map};
pub use shell_env_policy::{
EnvironmentVariablePattern, ShellEnvironmentPolicy, ShellEnvironmentPolicyInherit,
apply_shell_environment_policy,
};
pub use spawn::{
ProcessGroup, ProcessScope, detach_command, global_process_scope, new_process_group,
};

View file

@ -0,0 +1,237 @@
//! Controls which environment variables agent subprocesses (bash tool,
//! terminals) inherit. Default is a no-op (inherit everything); enforced at the
//! shell spawn sites on macOS, Linux, and Windows.
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::LazyLock;
use wildmatch::WildMatchPattern;
/// Case-insensitive environment-variable-name glob (`*`, `?`).
pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
fn deserialize_patterns<'de, D>(
deserializer: D,
) -> Result<Vec<EnvironmentVariablePattern>, D::Error>
where
D: serde::Deserializer<'de>,
{
let globs = Vec::<String>::deserialize(deserializer)?;
Ok(globs
.iter()
.map(|s| EnvironmentVariablePattern::new_case_insensitive(s))
.collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ShellEnvironmentPolicyInherit {
/// Core platform variables only (PATH, HOME, SHELL, ...).
Core,
#[default]
All,
None,
}
/// How to build the environment for agent subprocesses. Applied in order: start
/// from `inherit`; if `ignore_default_excludes` is false, drop the secret
/// patterns `*KEY*`/`*SECRET*`/`*TOKEN*`; drop `exclude`; insert `set`; if
/// `include_only` is non-empty, keep only those. Patterns are case-insensitive
/// globs (`*`, `?`).
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(default)]
pub struct ShellEnvironmentPolicy {
pub inherit: ShellEnvironmentPolicyInherit,
/// Skip the built-in secret excludes (default `true`).
pub ignore_default_excludes: bool,
#[serde(deserialize_with = "deserialize_patterns")]
pub exclude: Vec<EnvironmentVariablePattern>,
/// Values inserted into the base environment before `include_only` filtering
/// (an unmatched name is then dropped). These seed the base; request env
/// layered at spawn can still override them.
pub set: HashMap<String, String>,
#[serde(deserialize_with = "deserialize_patterns")]
pub include_only: Vec<EnvironmentVariablePattern>,
}
impl Default for ShellEnvironmentPolicy {
fn default() -> Self {
Self {
inherit: ShellEnvironmentPolicyInherit::All,
ignore_default_excludes: true,
exclude: Vec::new(),
set: HashMap::new(),
include_only: Vec::new(),
}
}
}
impl ShellEnvironmentPolicy {
/// True when the policy leaves the inherited environment untouched.
pub fn is_noop(&self) -> bool {
self.inherit == ShellEnvironmentPolicyInherit::All
&& self.ignore_default_excludes
&& self.exclude.is_empty()
&& self.set.is_empty()
&& self.include_only.is_empty()
}
/// True if `name` matches a built-in secret exclude and those are enabled.
fn matches_default_exclude(&self, name: &str) -> bool {
!self.ignore_default_excludes && DEFAULT_SECRET_EXCLUDES.iter().any(|p| p.matches(name))
}
fn matches_exclude(&self, name: &str) -> bool {
self.exclude.iter().any(|p| p.matches(name))
}
/// True if `include_only` is empty (all admitted) or `name` matches it.
fn matches_include_only(&self, name: &str) -> bool {
self.include_only.is_empty() || self.include_only.iter().any(|p| p.matches(name))
}
/// Whether `name` survives the name filters (default excludes, `exclude`,
/// `include_only`), ignoring `inherit`/`set`. Used to filter variables layered
/// in after the policy base, e.g. login-shell capture. Shares its matchers
/// with [`create_env_from_vars`] so the two cannot drift.
pub fn allows(&self, name: &str) -> bool {
!self.matches_default_exclude(name)
&& !self.matches_exclude(name)
&& self.matches_include_only(name)
}
/// Like [`allows`](Self::allows) but also honors `inherit`: `none` admits
/// nothing, `core` admits only core names, `all` defers to `allows`.
pub fn allows_with_inherit(&self, name: &str) -> bool {
match self.inherit {
ShellEnvironmentPolicyInherit::None => return false,
ShellEnvironmentPolicyInherit::Core => {
if !CORE_ENV_VARS
.iter()
.any(|core| core.eq_ignore_ascii_case(name))
{
return false;
}
}
ShellEnvironmentPolicyInherit::All => {}
}
self.allows(name)
}
}
/// Built-in secret excludes applied when `ignore_default_excludes` is false.
/// Shared by the base-env build and the login-capture filter so they can't drift.
static DEFAULT_SECRET_EXCLUDES: LazyLock<[EnvironmentVariablePattern; 3]> = LazyLock::new(|| {
[
EnvironmentVariablePattern::new_case_insensitive("*KEY*"),
EnvironmentVariablePattern::new_case_insensitive("*SECRET*"),
EnvironmentVariablePattern::new_case_insensitive("*TOKEN*"),
]
});
/// "Core" variables retained under [`ShellEnvironmentPolicyInherit::Core`].
#[cfg(not(target_os = "windows"))]
const CORE_ENV_VARS: &[&str] = &[
"PATH", "SHELL", "TMPDIR", "TEMP", "TMP", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME",
"USER",
];
#[cfg(target_os = "windows")]
const CORE_ENV_VARS: &[&str] = &[
"PATH",
"PATHEXT",
"SHELL",
"COMSPEC",
"SYSTEMROOT",
"SYSTEMDRIVE",
"USERNAME",
"USERDOMAIN",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"PROGRAMFILES",
"PROGRAMFILES(X86)",
"PROGRAMW6432",
"PROGRAMDATA",
"LOCALAPPDATA",
"APPDATA",
"TEMP",
"TMP",
"TMPDIR",
"POWERSHELL",
"PWSH",
];
/// Build the child environment from `policy` and the process env. Uses `vars_os`
/// and skips non-UTF-8 entries so a hostile variable cannot panic at spawn time.
pub(crate) fn create_env(policy: &ShellEnvironmentPolicy) -> HashMap<String, String> {
let vars = std::env::vars_os()
.filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?)));
create_env_from_vars(vars, policy)
}
pub(crate) fn create_env_from_vars<I>(
vars: I,
policy: &ShellEnvironmentPolicy,
) -> HashMap<String, String>
where
I: IntoIterator<Item = (String, String)>,
{
let mut env: HashMap<String, String> = match policy.inherit {
ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
ShellEnvironmentPolicyInherit::None => HashMap::new(),
ShellEnvironmentPolicyInherit::Core => vars
.into_iter()
.filter(|(k, _)| {
CORE_ENV_VARS
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(k))
})
.collect(),
};
// Order matters: default excludes, then `exclude`, then `set`, then
// `include_only`. `set` lands before `include_only` so an unmatched set name
// is still dropped. The matchers are shared with `allows`.
env.retain(|k, _| !policy.matches_default_exclude(k));
env.retain(|k, _| !policy.matches_exclude(k));
for (k, v) in &policy.set {
env.insert(k.clone(), v.clone());
}
env.retain(|k, _| policy.matches_include_only(k));
// Windows resolves executables via PATHEXT; keep it present even under a
// restrictive policy so commands stay runnable.
if cfg!(target_os = "windows") && !env.keys().any(|k| k.eq_ignore_ascii_case("PATHEXT")) {
env.insert("PATHEXT".to_string(), ".COM;.EXE;.BAT;.CMD".to_string());
}
env
}
/// Clear the command's inherited env and install the policy-derived base env.
/// `active` must already be noop-filtered; `None` leaves the command untouched.
/// The one base-env code path, shared by the public entry point and the spawn
/// sites.
pub(crate) fn install_policy_base_env(
cmd: &mut tokio::process::Command,
active: Option<&ShellEnvironmentPolicy>,
) {
if let Some(policy) = active {
cmd.env_clear();
cmd.envs(create_env(policy));
}
}
/// Install the policy-derived base env on `cmd` (clearing inherited env first);
/// a `None` or no-op policy leaves it untouched. Call before any other
/// `.env`/`.envs`.
pub fn apply_shell_environment_policy(
cmd: &mut tokio::process::Command,
policy: Option<&ShellEnvironmentPolicy>,
) {
install_policy_base_env(cmd, policy.filter(|p| !p.is_noop()));
}
#[cfg(test)]
#[path = "shell_env_policy_tests.rs"]
mod tests;

View file

@ -0,0 +1,166 @@
use super::{
EnvironmentVariablePattern, ShellEnvironmentPolicy, ShellEnvironmentPolicyInherit,
apply_shell_environment_policy, create_env_from_vars,
};
use std::collections::HashMap;
fn vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
fn patterns(globs: &[&str]) -> Vec<EnvironmentVariablePattern> {
globs
.iter()
.map(|g| EnvironmentVariablePattern::new_case_insensitive(g))
.collect()
}
#[test]
fn apply_policy_reshapes_command_env() {
let mut set = HashMap::new();
set.insert("MY_FLAG".to_string(), "1".to_string());
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::None,
set,
..Default::default()
};
let mut cmd = tokio::process::Command::new("true");
apply_shell_environment_policy(&mut cmd, Some(&policy));
let envs: HashMap<String, String> = cmd
.as_std()
.get_envs()
.filter_map(|(k, v)| Some((k.to_str()?.to_string(), v?.to_str()?.to_string())))
.collect();
assert_eq!(envs.get("MY_FLAG").map(String::as_str), Some("1"));
// inherit=None cleared the env, so no inherited PATH leaks through.
assert!(!envs.contains_key("PATH"));
}
#[test]
fn apply_noop_or_absent_policy_leaves_command_untouched() {
let mut cmd = tokio::process::Command::new("true");
apply_shell_environment_policy(&mut cmd, None);
apply_shell_environment_policy(&mut cmd, Some(&ShellEnvironmentPolicy::default()));
// No env_clear and no sets: the command carries no explicit env entries.
assert_eq!(cmd.as_std().get_envs().count(), 0);
}
#[test]
fn default_excludes_drop_secrets_when_enabled() {
let policy = ShellEnvironmentPolicy {
ignore_default_excludes: false,
..Default::default()
};
assert!(!policy.is_noop());
let env = create_env_from_vars(
vars(&[
("PATH", "/bin"),
("MY_API_KEY", "x"),
("MY_SECRET", "y"),
("GH_TOKEN", "z"),
]),
&policy,
);
assert_eq!(env.get("PATH").map(String::as_str), Some("/bin"));
assert!(!env.contains_key("MY_API_KEY"));
assert!(!env.contains_key("MY_SECRET"));
assert!(!env.contains_key("GH_TOKEN"));
}
#[test]
fn inherit_none_starts_empty_then_set_applies() {
let mut set = HashMap::new();
set.insert("PATH".to_string(), "/usr/bin".to_string());
set.insert("MY_FLAG".to_string(), "1".to_string());
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::None,
set,
..Default::default()
};
let env = create_env_from_vars(vars(&[("PATH", "/bin"), ("HOME", "/root")]), &policy);
assert_eq!(env.get("PATH").map(String::as_str), Some("/usr/bin"));
assert_eq!(env.get("MY_FLAG").map(String::as_str), Some("1"));
assert!(!env.contains_key("HOME"));
}
#[test]
fn inherit_core_keeps_only_core_vars() {
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::Core,
..Default::default()
};
let env = create_env_from_vars(vars(&[("PATH", "/bin"), ("RANDOM_VAR", "v")]), &policy);
assert_eq!(env.get("PATH").map(String::as_str), Some("/bin"));
assert!(!env.contains_key("RANDOM_VAR"));
}
#[test]
fn exclude_and_include_only_filter() {
let policy = ShellEnvironmentPolicy {
exclude: patterns(&["AWS_*"]),
include_only: patterns(&["PATH", "HOME"]),
..Default::default()
};
let env = create_env_from_vars(
vars(&[
("PATH", "/bin"),
("HOME", "/root"),
("AWS_SECRET", "s"),
("OTHER", "o"),
]),
&policy,
);
assert_eq!(env.get("PATH").map(String::as_str), Some("/bin"));
assert_eq!(env.get("HOME").map(String::as_str), Some("/root"));
assert!(!env.contains_key("AWS_SECRET"));
assert!(!env.contains_key("OTHER"));
}
#[test]
fn allows_filters_by_name_case_insensitively() {
let policy = ShellEnvironmentPolicy {
exclude: patterns(&["aws_*"]), // lowercase pattern, uppercase var
include_only: patterns(&["PATH", "HOME"]),
..Default::default()
};
assert!(policy.allows("PATH"));
assert!(!policy.allows("AWS_SECRET")); // excluded (case-insensitive)
assert!(!policy.allows("OTHER")); // not in include_only
let scrub = ShellEnvironmentPolicy {
ignore_default_excludes: false,
..Default::default()
};
assert!(!scrub.allows("my_api_key")); // `*KEY*` matches case-insensitively
assert!(ShellEnvironmentPolicy::default().allows("MY_API_KEY")); // default allows all
}
#[test]
fn allows_with_inherit_honors_inherit() {
// inherit = none admits nothing.
let none = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::None,
..Default::default()
};
assert!(!none.allows_with_inherit("PATH"));
assert!(!none.allows_with_inherit("FOO"));
// inherit = core admits only core names.
let core = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::Core,
..Default::default()
};
assert!(core.allows_with_inherit("PATH"));
assert!(!core.allows_with_inherit("RANDOM_VAR"));
// inherit = all defers to `allows` (exclude still applies).
let all = ShellEnvironmentPolicy {
exclude: patterns(&["AWS_*"]),
..Default::default()
};
assert!(all.allows_with_inherit("RANDOM_VAR"));
assert!(!all.allows_with_inherit("AWS_SECRET"));
}