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

@ -5,21 +5,59 @@
//! automatically.
use clap::Parser;
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
use xai_grok_workspace::config::WorkspaceServerMetadata;
use xai_grok_workspace::daemonize;
use xai_grok_workspace::diag_server;
use xai_grok_workspace::diag_server::{self, DiagHandle, ErrorClass};
use xai_grok_workspace::error::WorkspaceError;
use xai_grok_workspace::preview_supervisor::{self, PreviewArgs, PreviewVisibility};
/// OTLP `service.name` for this binary's exported traces/logs/metrics and
/// direct-OTLP fastrace export. Single source so the call sites can't drift.
const SERVICE_NAME: &str = "prod_grok_workspace";
const EXIT_SERVER_ID_INVALID: i32 = 3;
const INVALID_SERVER_ID_MARKER: &str = "workspace-server: invalid --server-id";
const WORKSPACE_HUB_AUTH_FAILED_MARKER: &str = "workspace hub auth failed";
/// Post-failure dwell so the host can poll `/ready` before exit ([500ms, 2s]).
const HUB_CONNECT_FAILED_DWELL: Duration = Duration::from_millis(750);
fn server_id_startup_error(id: &str) -> Option<String> {
id.parse::<xai_tool_protocol::ServerId>()
.err()
.map(|e| format!("{INVALID_SERVER_ID_MARKER} {id:?}: {e}"))
}
/// Classify hub-connect Display strings for `/ready` error_class.
/// Auth needles → `hub_auth`; other hub-connect path failures → `hub_connect`;
/// pre-hub workspace setup messages → `unknown` (still retryable alongside hub_connect).
fn classify_hub_connect_failure(err_msg: &str) -> ErrorClass {
if err_msg.contains("handshake auth failed") || err_msg.contains("auth error:") {
ErrorClass::HubAuth
} else if err_msg.contains("failed to create workspace") {
ErrorClass::Unknown
} else {
ErrorClass::HubConnect
}
}
/// Drop outer `hub error: ` so `/ready` detail is the inner failure text.
fn hub_connect_error_detail(err_msg: &str) -> &str {
err_msg.strip_prefix("hub error: ").unwrap_or(err_msg)
}
fn hub_connect_failure_log_message(class: ErrorClass) -> &'static str {
match class {
ErrorClass::HubAuth => WORKSPACE_HUB_AUTH_FAILED_MARKER,
ErrorClass::HubConnect | ErrorClass::Unknown => "failed to connect workspace to hub",
}
}
/// Mark `/ready` failed and dwell so the host can observe state before exit.
async fn report_hub_connect_failure(diag: &DiagHandle, err: &WorkspaceError) {
let err_msg = err.to_string();
let class = classify_hub_connect_failure(&err_msg);
diag.set_failed(class, hub_connect_error_detail(&err_msg));
tracing::error!(error = %err_msg, "{}", hub_connect_failure_log_message(class));
dwell_after_hub_connect_failed().await;
}
async fn dwell_after_hub_connect_failed() {
tokio::time::sleep(HUB_CONNECT_FAILED_DWELL).await;
}
#[derive(Parser)]
#[command(name = "xai-workspace-server")]
#[command(about = "Standalone workspace ToolServer for the server connection")]
@ -345,7 +383,7 @@ async fn run(args: Args, cwd: PathBuf) -> anyhow::Result<()> {
};
let preview_scrape_interval = status_config.preview_activity_scrape_interval;
xai_grok_workspace::init_metrics();
let ws_handle = xai_grok_workspace::handle::connect_local_workspace(
let ws_handle = match xai_grok_workspace::handle::connect_local_workspace(
cwd,
url,
auth_provider,
@ -361,7 +399,13 @@ async fn run(args: Args, cwd: PathBuf) -> anyhow::Result<()> {
args.confine_fs_to_workspace_root,
)
.await
.map_err(|e| anyhow::anyhow!("failed to connect workspace to hub: {e}"))?;
{
Ok(handle) => handle,
Err(e) => {
report_hub_connect_failure(&diag_handle, &e).await;
return Err(anyhow::anyhow!("failed to connect workspace to hub: {e}"));
}
};
if let Some((tx, control_port)) = &preview_shutdown {
tokio::spawn(preview_supervisor::supervise_preview_activity(
*control_port,
@ -447,6 +491,216 @@ async fn run(args: Args, cwd: PathBuf) -> anyhow::Result<()> {
mod tests {
use super::*;
#[test]
fn hub_connect_failed_dwell_is_within_design_bounds() {
assert!(HUB_CONNECT_FAILED_DWELL >= Duration::from_millis(500));
assert!(HUB_CONNECT_FAILED_DWELL <= Duration::from_secs(2));
}
#[tokio::test(start_paused = true)]
async fn hub_connect_failed_dwell_elapses_exact_budget() {
let start = tokio::time::Instant::now();
dwell_after_hub_connect_failed().await;
assert_eq!(start.elapsed(), HUB_CONNECT_FAILED_DWELL);
}
#[test]
fn classify_hub_connect_auth_needles() {
assert_eq!(
classify_hub_connect_failure("hub error: handshake auth failed: HTTP 401"),
ErrorClass::HubAuth
);
assert_eq!(
classify_hub_connect_failure("handshake auth failed: HTTP 401"),
ErrorClass::HubAuth
);
assert_eq!(
classify_hub_connect_failure("hub error: auth error: token rejected"),
ErrorClass::HubAuth
);
assert_eq!(
classify_hub_connect_failure("HTTP 401 unauthorized"),
ErrorClass::HubConnect
);
assert_eq!(
classify_hub_connect_failure("token refresh failed"),
ErrorClass::HubConnect
);
}
#[test]
fn classify_from_client_error_display_round_trip() {
let handshake = WorkspaceError::HubError(
xai_computer_hub_sdk::ClientError::HandshakeAuthFailed { status: 401 }.to_string(),
);
let handshake_msg = handshake.to_string();
assert_eq!(
classify_hub_connect_failure(&handshake_msg),
ErrorClass::HubAuth
);
assert_eq!(
hub_connect_failure_log_message(ErrorClass::HubAuth),
WORKSPACE_HUB_AUTH_FAILED_MARKER
);
let auth = WorkspaceError::HubError(
xai_computer_hub_sdk::ClientError::AuthError("token rejected".into()).to_string(),
);
assert_eq!(
classify_hub_connect_failure(&auth.to_string()),
ErrorClass::HubAuth
);
let network = WorkspaceError::HubError(
xai_computer_hub_sdk::ClientError::NetworkError("connection refused".into())
.to_string(),
);
assert_eq!(
classify_hub_connect_failure(&network.to_string()),
ErrorClass::HubConnect
);
assert_ne!(
hub_connect_failure_log_message(ErrorClass::HubConnect),
WORKSPACE_HUB_AUTH_FAILED_MARKER
);
}
#[test]
fn classify_hub_connect_non_auth_is_hub_connect() {
assert_eq!(
classify_hub_connect_failure("hub error: network error: connection refused"),
ErrorClass::HubConnect
);
assert_eq!(
classify_hub_connect_failure("hub error: protocol error: bad hello"),
ErrorClass::HubConnect
);
assert_eq!(
classify_hub_connect_failure("failed to create workspace: disk full"),
ErrorClass::Unknown
);
}
#[test]
fn hub_auth_marker_is_stable_literal() {
assert_eq!(
WORKSPACE_HUB_AUTH_FAILED_MARKER,
"workspace hub auth failed"
);
}
#[test]
fn hub_connect_error_detail_strips_hub_error_prefix() {
let err = WorkspaceError::HubError("handshake auth failed: HTTP 401".into());
assert_eq!(
hub_connect_error_detail(&err.to_string()),
"handshake auth failed: HTTP 401"
);
let other = WorkspaceError::HubError("network error: timeout".into());
assert_eq!(
hub_connect_error_detail(&other.to_string()),
"network error: timeout"
);
}
/// Install a capturing tracing subscriber for the duration of an async
/// report; returns emitted event messages.
async fn report_with_captured_messages(
handle: &DiagHandle,
err: &WorkspaceError,
) -> (Duration, Vec<String>) {
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing_subscriber::layer::{Context, SubscriberExt as _};
use tracing_subscriber::{Layer, Registry};
#[derive(Default)]
struct MsgVisitor {
message: Option<String>,
}
impl Visit for MsgVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{value:?}").trim_matches('"').to_owned());
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_owned());
}
}
}
struct CaptureLayer {
msgs: Arc<Mutex<Vec<String>>>,
}
impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let mut v = MsgVisitor::default();
event.record(&mut v);
if let Some(msg) = v.message {
self.msgs
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(msg);
}
}
}
let msgs = Arc::new(Mutex::new(Vec::new()));
let subscriber = Registry::default().with(CaptureLayer { msgs: msgs.clone() });
let _guard = tracing::subscriber::set_default(subscriber);
let start = tokio::time::Instant::now();
report_hub_connect_failure(handle, err).await;
let elapsed = start.elapsed();
let messages = msgs.lock().unwrap_or_else(|e| e.into_inner()).clone();
(elapsed, messages)
}
#[tokio::test(start_paused = true)]
async fn report_hub_connect_failure_sets_ready_failed_auth_and_dwells() {
let handle = DiagHandle::new(Some("nonce-auth".to_owned()));
let bound = diag_server::serve(diag_server::DiagListener::Tcp(0), handle.clone(), None)
.await
.expect("bind");
let port = bound.port.expect("tcp port");
let err = WorkspaceError::HubError("handshake auth failed: HTTP 401".into());
let (elapsed, messages) = report_with_captured_messages(&handle, &err).await;
assert_eq!(elapsed, HUB_CONNECT_FAILED_DWELL);
assert!(
messages
.iter()
.any(|m| m == WORKSPACE_HUB_AUTH_FAILED_MARKER),
"auth path must emit marker, got {messages:?}"
);
let response = reqwest::get(format!("http://127.0.0.1:{port}/ready"))
.await
.expect("request");
assert_eq!(response.status().as_u16(), 503);
let body: serde_json::Value = response.json().await.expect("json");
assert_eq!(body["state"], "failed");
assert_eq!(body["error_class"], "hub_auth");
assert_eq!(body["error_detail"], "handshake auth failed: HTTP 401");
assert_eq!(body["launch_id"], "nonce-auth");
}
#[tokio::test(start_paused = true)]
async fn report_hub_connect_failure_sets_ready_failed_hub_connect() {
let handle = DiagHandle::new(None);
let bound = diag_server::serve(diag_server::DiagListener::Tcp(0), handle.clone(), None)
.await
.expect("bind");
let port = bound.port.expect("tcp port");
let err = WorkspaceError::HubError("network error: connection refused".into());
let (elapsed, messages) = report_with_captured_messages(&handle, &err).await;
assert_eq!(elapsed, HUB_CONNECT_FAILED_DWELL);
assert!(
messages
.iter()
.any(|m| m == "failed to connect workspace to hub"),
"non-auth path must emit connect failure line, got {messages:?}"
);
assert!(
messages
.iter()
.all(|m| m != WORKSPACE_HUB_AUTH_FAILED_MARKER),
"non-auth path must not emit auth marker, got {messages:?}"
);
let response = reqwest::get(format!("http://127.0.0.1:{port}/ready"))
.await
.expect("request");
assert_eq!(response.status().as_u16(), 503);
let body: serde_json::Value = response.json().await.expect("json");
assert_eq!(body["state"], "failed");
assert_eq!(body["error_class"], "hub_connect");
assert_eq!(body["error_detail"], "network error: connection refused");
}
#[test]
fn capabilities_flag_parses_and_defaults_off() {
let args = Args::try_parse_from(["xai-workspace-server"]).unwrap();
assert!(!args.capabilities);