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