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

@ -21,19 +21,16 @@ use super::token_output::{expiry_after_seconds, parse_token_output};
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(default)]
pub struct AuthProviderConfig {
/// Command that prints a bearer token on stdout, bare or as JSON
/// `{access_token, expires_in}`. Without `args` it runs via `sh -c`.
/// Command to run; without `args` it uses the platform shell, with `args` it execs directly.
pub command: String,
/// Arguments for `command`. When present (even empty), the command runs
/// directly with no shell; `command` is a program name on `PATH`, or a path.
/// Command arguments; when set (even empty) the command execs directly.
pub args: Option<Vec<String>>,
/// Fallback token lifetime in seconds, used when the command's output
/// carries no `expires_in`. Takes precedence over a JWT `exp` claim.
/// Fallback token lifetime used when the output carries no `expires_in`.
pub token_ttl_secs: Option<u64>,
/// Maximum seconds to wait for the command (default 30, clamped to 1..=600).
/// A turn waits up to this long on a mint, so keep helpers fast and
/// non-interactive.
/// Max seconds to wait for the command (default 30, clamped to 1..=600).
pub timeout_secs: Option<u64>,
/// Working directory for the command; a leading `~` expands to home.
pub cwd: Option<String>,
}
impl AuthProviderConfig {
@ -210,14 +207,17 @@ const PROVIDER_STDERR_CAP_BYTES: u64 = 64 << 10; // 64 KiB
/// new `AuthProviderConfig` field is a compile error until it is classified as
/// token-shaping (add it here) or an execution knob like `timeout_secs`
/// (editing it never invalidates).
fn token_identity(config: &AuthProviderConfig) -> (&str, Option<&[String]>, Option<u64>) {
fn token_identity(
config: &AuthProviderConfig,
) -> (&str, Option<&[String]>, Option<u64>, Option<&str>) {
let AuthProviderConfig {
command,
args,
token_ttl_secs,
timeout_secs: _,
cwd,
} = config;
(command, args.as_deref(), *token_ttl_secs)
(command, args.as_deref(), *token_ttl_secs, cwd.as_deref())
}
fn minted_token_is_stale(minted: &MintedProviderToken, config: &AuthProviderConfig) -> bool {
@ -333,6 +333,19 @@ async fn run_capped(
})
}
fn resolve_program(command: &str, cwd: Option<&std::path::Path>) -> std::path::PathBuf {
let path = std::path::Path::new(command);
if path.is_absolute() {
return path.to_path_buf();
}
if path.components().count() > 1
&& let Some(dir) = cwd
{
return dir.join(path);
}
std::path::PathBuf::from(command)
}
async fn mint_provider_token(
provider: &AuthProviderRef,
mark_expired: bool,
@ -356,20 +369,33 @@ async fn mint_provider_token(
"auth provider: running helper command"
);
let cwd = config
.cwd
.as_deref()
.map(str::trim)
.filter(|c| !c.is_empty())
.map(crate::util::expand_home);
let mut cmd = match config.args {
Some(ref args) => {
// Direct exec: the program name is a PATH lookup, so trim stray
// whitespace that would otherwise fail to resolve.
let mut cmd = tokio::process::Command::new(config.command.trim());
let program = resolve_program(config.command.trim(), cwd.as_deref());
let mut cmd = tokio::process::Command::new(program);
cmd.args(args);
cmd
}
None => {
let mut cmd = tokio::process::Command::new("sh");
cmd.args(["-c", &config.command]);
#[cfg(windows)]
let (shell, flag) = ("cmd", "/C");
#[cfg(not(windows))]
let (shell, flag) = ("sh", "-c");
let mut cmd = tokio::process::Command::new(shell);
cmd.args([flag, config.command.as_str()]);
cmd
}
};
if let Some(ref dir) = cwd {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
// Capture stderr for the failure log; inheriting corrupts the TUI.
@ -613,6 +639,7 @@ pub(crate) fn test_counting_provider(name: &str, dir: &std::path::Path) -> AuthP
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
)
}

View file

@ -149,6 +149,7 @@ async fn provider_config_edit_invalidates_cached_token() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
assert_eq!(
@ -181,6 +182,7 @@ async fn provider_401_recovery_reminted_under_edited_config() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
assert_eq!(
@ -205,6 +207,7 @@ async fn provider_timeout_edit_does_not_invalidate_token() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: Some(5),
cwd: None,
},
);
assert_eq!(
@ -214,6 +217,31 @@ async fn provider_timeout_edit_does_not_invalidate_token() {
);
}
/// `cwd` is part of `token_identity`, so editing it invalidates the cache: the
/// same helper in a different directory can mint a different token.
#[tokio::test]
async fn provider_cwd_edit_invalidates_cached_token() {
let dir = tempfile::tempdir().unwrap();
let provider = counting_provider("test-cwd-edit", dir.path());
provider.ensure_fresh_token(None).await.rotated().unwrap();
let moved = AuthProviderRef::new(
"test-cwd-edit".to_owned(),
AuthProviderConfig {
command: provider.config.command.clone(),
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: Some("/some/other/dir".to_owned()),
},
);
assert_eq!(
moved.cached_token(),
None,
"a cwd edit must invalidate the cached token"
);
}
#[tokio::test]
async fn attach_trusted_config_lets_a_revived_ref_mint() {
let dir = tempfile::tempdir().unwrap();
@ -296,6 +324,7 @@ async fn provider_refresh_sets_expired_env() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
assert_eq!(
@ -325,6 +354,7 @@ async fn provider_concurrent_mints_single_flight() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
let (a, b) = tokio::join!(
@ -373,6 +403,7 @@ async fn provider_expiry_source_precedence() {
args: None,
token_ttl_secs,
timeout_secs: None,
cwd: None,
},
);
let first = provider
@ -430,6 +461,7 @@ async fn provider_unusable_expiry_still_mints() {
args: None,
token_ttl_secs: Some(u64::MAX),
timeout_secs: None,
cwd: None,
},
);
assert_eq!(
@ -454,6 +486,7 @@ async fn provider_args_run_without_a_shell() {
args: Some(vec!["tok-$HOME;42".to_owned()]),
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
assert_eq!(
@ -471,6 +504,7 @@ async fn provider_command_times_out() {
args: None,
token_ttl_secs: None,
timeout_secs: Some(1),
cwd: None,
},
);
let start = std::time::Instant::now();
@ -496,6 +530,7 @@ async fn provider_zero_timeout_clamps_to_one_second() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: Some(0),
cwd: None,
},
);
assert_eq!(
@ -512,6 +547,7 @@ async fn provider_zero_timeout_clamps_to_one_second() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: Some(0),
cwd: None,
},
);
assert!(
@ -534,6 +570,7 @@ async fn mint_error_messages_distinguish_failure_modes() {
args: None,
token_ttl_secs: None,
timeout_secs: Some(1),
cwd: None,
},
);
let err = mint_provider_token(&timed_out, false, None)
@ -549,6 +586,7 @@ async fn mint_error_messages_distinguish_failure_modes() {
args: Some(vec![]),
token_ttl_secs: None,
timeout_secs: Some(5),
cwd: None,
},
);
let err = mint_provider_token(&missing, false, None)
@ -564,6 +602,7 @@ async fn mint_error_messages_distinguish_failure_modes() {
args: None,
token_ttl_secs: None,
timeout_secs: Some(5),
cwd: None,
},
);
let err = mint_provider_token(&empty_output, false, None)
@ -585,6 +624,7 @@ async fn re_mint_hands_the_prior_token_back_to_the_command() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
@ -620,6 +660,7 @@ async fn failed_401_remint_invalidates_the_cached_token() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
@ -658,6 +699,7 @@ async fn failed_pre_turn_mint_does_not_serve_the_stale_token() {
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: None,
},
);
@ -689,6 +731,7 @@ async fn provider_output_over_cap_fails_closed() {
args: None,
token_ttl_secs: None,
timeout_secs: Some(5),
cwd: None,
},
);
let err = mint_provider_token(&provider, false, None)
@ -761,3 +804,85 @@ async fn provider_helper_env_scrubs_first_party_credentials() {
"no first-party credential may survive into the helper env"
);
}
/// `resolve_program` branches: bare name via `PATH`, absolute as-is, relative
/// against `cwd`.
#[test]
fn resolve_program_resolves_against_cwd() {
let cwd = std::path::Path::new("/work");
assert_eq!(
super::resolve_program("token-helper", Some(cwd)),
std::path::PathBuf::from("token-helper")
);
let abs = if cfg!(windows) {
r"C:\bin\helper.exe"
} else {
"/usr/local/bin/helper"
};
assert_eq!(
super::resolve_program(abs, Some(cwd)),
std::path::PathBuf::from(abs)
);
assert_eq!(
super::resolve_program("bin/helper", Some(cwd)),
cwd.join("bin/helper")
);
assert_eq!(
super::resolve_program("bin/helper", None),
std::path::PathBuf::from("bin/helper"),
"with no cwd a relative path is left to the process cwd"
);
}
/// The `args` form (the portable, no-shell shape a desktop/Windows helper
/// should use) resolves a relative program against the provider's `cwd`.
#[cfg(unix)]
#[tokio::test]
async fn provider_resolves_relative_program_against_cwd() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("token.sh");
std::fs::write(&script, "#!/bin/sh\nprintf 'cwd-tok'\n").unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let provider = AuthProviderRef::new(
"test-cwd-relative".to_owned(),
AuthProviderConfig {
command: "./token.sh".to_owned(),
args: Some(vec![]),
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: Some(dir.path().to_string_lossy().into_owned()),
},
);
assert_eq!(
provider.ensure_fresh_token(None).await.rotated().as_deref(),
Some("cwd-tok")
);
}
/// `cwd` is the command's runtime directory: reading a file by relative name
/// only succeeds if `current_dir` took effect (here via the shell form).
#[cfg(unix)]
#[tokio::test]
async fn provider_command_runs_in_cwd() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("token.txt"), "file-tok").unwrap();
let provider = AuthProviderRef::new(
"test-cwd-shell".to_owned(),
AuthProviderConfig {
command: "cat token.txt".to_owned(),
args: None,
token_ttl_secs: Some(3600),
timeout_secs: None,
cwd: Some(dir.path().to_string_lossy().into_owned()),
},
);
assert_eq!(
provider.ensure_fresh_token(None).await.rotated().as_deref(),
Some("file-tok")
);
}