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
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue