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

@ -90,6 +90,8 @@ mod tests {
api_backend: ApiBackend::ChatCompletions,
auth_scheme: Default::default(),
extra_headers: IndexMap::new(),
query_params: IndexMap::new(),
env_http_headers: IndexMap::new(),
context_window: 8192,
force_http1: false,
max_retries: None,

View file

@ -15,6 +15,7 @@
use eventsource_stream::Eventsource;
use futures_util::StreamExt;
use futures_util::stream::BoxStream;
use indexmap::IndexMap;
use reqwest::header::{
ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT,
};
@ -272,9 +273,38 @@ struct StreamOptions {
include_usage: bool,
}
/// Resolve `env_http_headers` (`header -> env var`) into `headers` via `getenv`, skipping unset/blank/invalid entries and trimming values.
fn apply_env_http_headers(
env_http_headers: &IndexMap<String, String>,
getenv: impl Fn(&str) -> Option<String>,
headers: &mut HeaderMap,
) {
for (key, env_var) in env_http_headers {
let Some(value) = getenv(env_var) else {
continue;
};
let value = value.trim();
if value.is_empty() {
continue;
}
let (Ok(name), Ok(header_value)) = (
HeaderName::try_from(key.as_str()),
HeaderValue::from_str(value),
) else {
tracing::warn!(
header = %key,
env_var = %env_var,
"skipping env_http_header with an invalid header name or value"
);
continue;
};
headers.insert(name, header_value);
}
}
/// HTTP client for sampling. Cheap to clone; carries an `Arc`-backed
/// `reqwest::Client` and the default headers/request-defaults computed
/// from a [`SamplerConfig`] at construction time.
/// `reqwest::Client` and the default headers/request-defaults computed from a
/// [`SamplerConfig`] at construction time.
#[derive(Clone)]
pub struct SamplingClient {
http: reqwest::Client,
@ -290,6 +320,8 @@ pub struct SamplingClient {
bearer_resolver: Option<crate::config::SharedBearerResolver>,
/// Per-request header injection (OTel traceparent).
header_injector: Option<crate::config::SharedHeaderInjector>,
/// Endpoint URL builder, resolved once from `base_url` + `query_params`.
endpoint: EndpointTemplate,
}
impl std::fmt::Debug for SamplingClient {
@ -318,6 +350,74 @@ struct ClientDefaults {
doom_loop_recovery: Option<xai_grok_sampling_types::DoomLoopRecoveryPolicy>,
}
/// Endpoint URL builder, resolved once at client construction so each request
/// only appends its path.
#[derive(Clone, Debug)]
enum EndpointTemplate {
/// No query params and no query on the base URL (or an unparseable base):
/// append the path to the base verbatim.
Plain(String),
/// Query params configured: `{prefix}/{path}{suffix}`. `suffix` starts with
/// `?` and folds any base-URL params, with a configured key winning over the
/// same key in `base_url` (percent-encoded, no duplicates).
WithQuery { prefix: String, suffix: String },
}
impl EndpointTemplate {
fn new(base_url: &str, query_params: &IndexMap<String, String>) -> Self {
let base = base_url.trim_end_matches('/').to_string();
// The fast path is safe only when there is nothing to fold: no configured
// params and no query already on the base (which would otherwise land
// before the appended path).
if query_params.is_empty() && !base.contains('?') {
return Self::Plain(base);
}
let mut url = match reqwest::Url::parse(&base) {
Ok(url) => url,
Err(error) => {
tracing::warn!(
url = %base,
%error,
"failed to parse base URL for endpoint; sending without folded query"
);
return Self::Plain(base);
}
};
let overridden: std::collections::HashSet<&str> =
query_params.keys().map(String::as_str).collect();
let kept: Vec<(String, String)> = url
.query_pairs()
.filter(|(k, _)| !overridden.contains(k.as_ref()))
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
let prefix = {
let mut prefix_url = url.clone();
prefix_url.set_query(None);
prefix_url.as_str().trim_end_matches('/').to_string()
};
{
let mut pairs = url.query_pairs_mut();
pairs.clear();
for (key, value) in &kept {
pairs.append_pair(key, value);
}
for (key, value) in query_params {
pairs.append_pair(key, value);
}
}
let suffix = url.query().map(|q| format!("?{q}")).unwrap_or_default();
Self::WithQuery { prefix, suffix }
}
fn url_for_path(&self, path: &str) -> String {
let path = path.trim_start_matches('/');
match self {
Self::Plain(base) => format!("{base}/{path}"),
Self::WithQuery { prefix, suffix } => format!("{prefix}/{path}{suffix}"),
}
}
}
// =============================================================================
// User-Agent helpers
// =============================================================================
@ -444,6 +544,14 @@ impl SamplingClient {
headers.insert(header_name, header_value);
}
// Resolve here, not into `extra_headers`, so an env-sourced secret stays
// out of persisted state.
apply_env_http_headers(
&config.env_http_headers,
|var| std::env::var(var).ok(),
&mut headers,
);
// Add x-grok-client-version header for version gating at the proxy.
if let Some(client_version) = config.client_version.as_ref()
&& let Ok(header_value) = HeaderValue::from_str(client_version)
@ -530,6 +638,8 @@ impl SamplingClient {
doom_loop_recovery: config.doom_loop_recovery,
};
let endpoint = EndpointTemplate::new(&config.base_url, &config.query_params);
Ok(Self {
http,
default_headers: headers,
@ -538,6 +648,7 @@ impl SamplingClient {
attribution_callback: config.attribution_callback,
bearer_resolver: config.bearer_resolver,
header_injector: config.header_injector,
endpoint,
})
}
@ -701,9 +812,7 @@ impl SamplingClient {
}
fn endpoint(&self, path: &str) -> String {
let base = self.base_url.trim_end_matches('/');
let path = path.trim_start_matches('/');
format!("{base}/{path}")
self.endpoint.url_for_path(path)
}
fn apply_defaults(&self, mut request: ChatCompletionRequest) -> Result<ChatCompletionRequest> {
@ -1907,6 +2016,8 @@ mod tests {
api_backend: ApiBackend::ChatCompletions,
auth_scheme: AuthScheme::Bearer,
extra_headers: IndexMap::new(),
query_params: IndexMap::new(),
env_http_headers: IndexMap::new(),
context_window: 8192,
force_http1: false,
max_retries: None,
@ -2079,6 +2190,56 @@ mod tests {
let _client = SamplingClient::new(cfg).expect("client with extra headers should construct");
}
#[test]
fn apply_env_http_headers_resolves_trims_skips_and_overrides() {
let mut map = IndexMap::new();
map.insert("x-tenant-token".to_string(), "TENANT".to_string());
map.insert("x-blank".to_string(), "BLANK".to_string());
map.insert("x-missing".to_string(), "MISSING".to_string());
map.insert("x-override".to_string(), "OVERRIDE".to_string());
map.insert("x invalid".to_string(), "INVALID".to_string());
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-override"),
HeaderValue::from_static("static"),
);
apply_env_http_headers(
&map,
|var| match var {
// Leading space + trailing newline exercises trimming.
"TENANT" => Some(" tenant-secret\n".to_string()),
"BLANK" => Some(" ".to_string()),
"OVERRIDE" => Some("from-env".to_string()),
"INVALID" => Some("value".to_string()),
_ => None,
},
&mut headers,
);
assert_eq!(headers.get("x-tenant-token").unwrap(), "tenant-secret");
assert!(headers.get("x-blank").is_none());
assert!(headers.get("x-missing").is_none());
// A resolved env value overrides an existing header of the same name.
assert_eq!(headers.get("x-override").unwrap(), "from-env");
// An invalid header name is skipped rather than panicking.
assert!(headers.get("x invalid").is_none());
}
#[test]
fn endpoint_appends_path_before_a_base_url_query_without_configured_params() {
let template =
EndpointTemplate::new("https://gateway.example/v1?api-version=x", &IndexMap::new());
let url = template.url_for_path("responses");
assert!(
url.starts_with("https://gateway.example/v1/responses?"),
"url: {url}"
);
assert!(url.contains("api-version=x"), "url: {url}");
assert!(!url.contains("x/responses"), "url: {url}");
}
#[test]
fn messages_plus_anthropic_api_key_uses_x_api_key_and_not_authorization() {
let cfg = SamplerConfig {

View file

@ -60,6 +60,13 @@ pub struct SamplerConfig {
/// the URL to derive headers; callers (the session) inject proxy auth
/// and other access headers here before constructing the config.
pub extra_headers: IndexMap<String, String>,
/// Query parameters folded into every request URL (percent-encoded).
#[serde(default)]
pub query_params: IndexMap<String, String>,
/// Header name to environment variable, resolved into request headers at
/// client build and never persisted.
#[serde(default)]
pub env_http_headers: IndexMap<String, String>,
/// Total context window size in tokens. The sampler does not enforce
/// it; it is informational metadata used by the session for compaction
/// decisions.
@ -140,6 +147,8 @@ impl Default for SamplerConfig {
api_backend: ApiBackend::default(),
auth_scheme: AuthScheme::default(),
extra_headers: IndexMap::new(),
query_params: IndexMap::new(),
env_http_headers: IndexMap::new(),
context_window: 0,
force_http1: false,
max_retries: None,

View file

@ -0,0 +1,64 @@
//! Checks that provider `query_params` and `env_http_headers` reach the
//! outgoing request.
mod support;
use std::sync::{Arc, Mutex};
use axum::Router;
use axum::http::{HeaderMap, Uri};
use axum::routing::post;
use tokio::net::TcpListener;
use xai_grok_sampler::SamplingClient;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_carries_query_params_and_env_http_headers() {
// A unique name avoids clashing with other tests that read the process
// environment; the surrounding whitespace exercises value trimming.
let env_var = "XAI_SAMPLER_TEST_TENANT_TOKEN";
unsafe { std::env::set_var(env_var, " tenant-secret\n") };
let captured: Arc<Mutex<Option<(String, HeaderMap)>>> = Arc::new(Mutex::new(None));
let sink = Arc::clone(&captured);
let app = Router::new().route(
"/v1/chat/completions",
post(move |uri: Uri, headers: HeaderMap| {
let sink = Arc::clone(&sink);
async move {
*sink.lock().unwrap() =
Some((uri.query().unwrap_or_default().to_string(), headers));
"{}"
}
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
// The base URL already carries `api-version`; a configured value must
// replace it (not duplicate), keep unrelated keys, and percent-encode.
let base_url = format!("http://{addr}/v1?api-version=old&keep=1");
let mut cfg = support::test_config(&base_url, "test-key");
cfg.query_params
.insert("api-version".into(), "2026-07-22".into());
cfg.query_params.insert("tenant".into(), "a b".into());
cfg.env_http_headers
.insert("x-tenant-token".into(), env_var.into());
let client = SamplingClient::new(cfg).expect("client builds");
support::send_one(&client).await;
unsafe { std::env::remove_var(env_var) };
let (query, headers) = captured.lock().unwrap().take().expect("request captured");
assert_eq!(query.matches("api-version=").count(), 1, "query: {query}");
assert!(query.contains("api-version=2026-07-22"), "query: {query}");
assert!(!query.contains("api-version=old"), "query: {query}");
assert!(query.contains("keep=1"), "query: {query}");
assert!(
query.contains("tenant=a%20b") || query.contains("tenant=a+b"),
"query: {query}"
);
assert_eq!(headers.get("x-tenant-token").unwrap(), "tenant-secret");
}

View file

@ -79,6 +79,8 @@ fn test_config(base_url: String, model: &str) -> SamplerConfig {
api_backend: ApiBackend::ChatCompletions,
auth_scheme: Default::default(),
extra_headers: IndexMap::new(),
query_params: IndexMap::new(),
env_http_headers: IndexMap::new(),
context_window: 128_000,
force_http1: false,
// Keep retries minimal so tests don't take forever.