Synced from monorepo
Changes: - grok-shell: request workspaces:read/write OAuth2 scopes - security: fix SSRF bypass via HTTP redirect in hook runner - fix(grok-build): enterprise STT WSS URL + API-key voice bearer - Harden identity-change purge and sync-marker invariants - sandbox + workspace-server: delete the legacy ready-file arm - Show billing URL when browser cannot open - fix(pager): show folder-trust UI in minimal mode - fix(pager): drain task_backgrounded before no-wait headless exit - grok-agent-sdk: stop SDK-spawned agents from staging self-updates they can never adopt - Split settings_modal into directory module - Delegate VS Code SSH file links - grok-shell: release the workspace session binding when a session is removed - keep skills reachable when their name collides with a client builtin - Preserve semantic link targets
This commit is contained in:
parent
c68e39f604
commit
8adf9013a0
117 changed files with 16998 additions and 14540 deletions
|
|
@ -45,10 +45,10 @@ struct Args {
|
|||
/// Propagated to `ServerInfo.metadata` in `servers.list` responses.
|
||||
#[arg(long)]
|
||||
metadata: Option<String>,
|
||||
/// Path to write a PID file once the server connection is established.
|
||||
/// The sandbox service polls this file to determine readiness.
|
||||
#[arg(long, default_value = daemonize::DEFAULT_READY_PATH)]
|
||||
ready_file: PathBuf,
|
||||
/// Deprecated no-op, accepted for one release so existing callers don't
|
||||
/// trip clap: nothing writes or reads this path.
|
||||
#[arg(long, hide = true)]
|
||||
ready_file: Option<PathBuf>,
|
||||
/// Unix-socket path for the in-guest diagnostics HTTP server
|
||||
/// (`/ready`, `/statusz`).
|
||||
#[cfg(unix)]
|
||||
|
|
@ -78,9 +78,7 @@ struct Args {
|
|||
)]
|
||||
upload_queue_enabled: bool,
|
||||
/// Fail `session.bind`s without an explicit toolset closed (RPC-only)
|
||||
/// instead of widening to the built-in default catalog. Passed by the
|
||||
/// sandbox service; doubles as a version tripwire (a stale revived binary
|
||||
/// rejects the argv and never reports ready).
|
||||
/// instead of widening to the built-in default catalog.
|
||||
#[arg(long)]
|
||||
require_explicit_toolset: bool,
|
||||
/// Confine `x.ai/fs/*` resolution to the workspace root (reject `..`,
|
||||
|
|
@ -189,7 +187,6 @@ fn main() -> anyhow::Result<()> {
|
|||
let anchor = |p: PathBuf| if p.is_absolute() { p } else { cwd.join(p) };
|
||||
args.log_file = anchor(std::mem::take(&mut args.log_file));
|
||||
args.pid_file = anchor(std::mem::take(&mut args.pid_file));
|
||||
args.ready_file = anchor(std::mem::take(&mut args.ready_file));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
args.diag_socket = anchor(std::mem::take(&mut args.diag_socket));
|
||||
|
|
@ -354,7 +351,6 @@ async fn run(args: Args, cwd: PathBuf) -> anyhow::Result<()> {
|
|||
status_config,
|
||||
args.upload_queue_enabled,
|
||||
project_lsp_trusted,
|
||||
Some(args.ready_file.clone()),
|
||||
Some(diag_handle.clone()),
|
||||
args.require_explicit_toolset,
|
||||
args.confine_fs_to_workspace_root,
|
||||
|
|
@ -413,7 +409,6 @@ async fn run(args: Args, cwd: PathBuf) -> anyhow::Result<()> {
|
|||
if let Some((tx, _)) = &preview_shutdown {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
let _ = std::fs::remove_file(&args.ready_file);
|
||||
diag_handle.set_shutting_down();
|
||||
tracing::info!("Received shutdown signal, draining...");
|
||||
let tracker = ws_handle.activity_tracker().clone();
|
||||
|
|
@ -482,10 +477,13 @@ mod tests {
|
|||
args.pid_file,
|
||||
PathBuf::from(daemonize::DEFAULT_PIDFILE_PATH)
|
||||
);
|
||||
assert_eq!(
|
||||
args.ready_file,
|
||||
PathBuf::from(daemonize::DEFAULT_READY_PATH)
|
||||
);
|
||||
assert_eq!(args.ready_file, None);
|
||||
}
|
||||
#[test]
|
||||
fn ready_file_is_accepted_as_a_deprecated_no_op() {
|
||||
let args =
|
||||
Args::try_parse_from(["xai-workspace-server", "--ready-file", "/tmp/x.ready"]).unwrap();
|
||||
assert_eq!(args.ready_file, Some(PathBuf::from("/tmp/x.ready")));
|
||||
}
|
||||
#[test]
|
||||
fn invalid_server_id_produces_the_marker_line() {
|
||||
|
|
|
|||
|
|
@ -40,13 +40,6 @@ pub const DEFAULT_PIDFILE_PATH: &str = "/tmp/workspace-server.pid";
|
|||
#[cfg(windows)]
|
||||
pub const DEFAULT_PIDFILE_PATH: &str = "C:\\Windows\\Temp\\workspace-server.pid";
|
||||
|
||||
/// Readiness marker written once the server connection is established; the control
|
||||
/// plane polls it and may override the path with `--ready-file`.
|
||||
#[cfg(unix)]
|
||||
pub const DEFAULT_READY_PATH: &str = "/tmp/workspace-server.ready";
|
||||
#[cfg(windows)]
|
||||
pub const DEFAULT_READY_PATH: &str = "C:\\Windows\\Temp\\workspace-server.ready";
|
||||
|
||||
/// How long a takeover waits for the gracefully-terminated predecessor to
|
||||
/// release the pidfile lock before escalating to a forceful kill.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ use tokio::net::TcpListener;
|
|||
use tokio::net::UnixListener;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Default Unix socket path (resolves host-visibly under the production
|
||||
/// launcher's `/tmp` bind mount, next to the ready/pid files).
|
||||
/// Default Unix socket path (next to the log/pid files).
|
||||
#[cfg(unix)]
|
||||
pub const DEFAULT_DIAG_SOCKET_PATH: &str = "/tmp/workspace-server.sock";
|
||||
|
||||
|
|
@ -137,11 +136,6 @@ impl DiagHandle {
|
|||
inner.state_changed_at = now_ms();
|
||||
}
|
||||
|
||||
/// True after [`Self::set_shutting_down`].
|
||||
pub fn is_shutting_down(&self) -> bool {
|
||||
self.lock().shutting_down
|
||||
}
|
||||
|
||||
fn lock(&self) -> MutexGuard<'_, Inner> {
|
||||
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
|
@ -416,7 +410,6 @@ mod tests {
|
|||
let (status, body) = get_json(port, "/ready").await;
|
||||
assert_eq!(status, 503);
|
||||
assert_eq!(body["state"], "disconnected");
|
||||
assert!(handle.is_shutting_down());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
|
|||
|
|
@ -156,9 +156,22 @@ impl ApprovedRoot {
|
|||
|
||||
fn relative_path(&self, path: &Path) -> Option<PathBuf> {
|
||||
let relative = if path.is_absolute() {
|
||||
path.strip_prefix(&self.path).ok()?
|
||||
// `self.path` is always canonical. Prefer a pure strip so openat
|
||||
// paths still resolve after the on-disk entry is replaced
|
||||
// (symlink swap); fall back to canonicalize for non-canonical
|
||||
// absolute inputs.
|
||||
path.strip_prefix(&self.path)
|
||||
.ok()
|
||||
.map(|r| r.to_path_buf())
|
||||
.or_else(|| {
|
||||
let absolute = dunce::canonicalize(path).ok()?;
|
||||
absolute
|
||||
.strip_prefix(&self.path)
|
||||
.ok()
|
||||
.map(|r| r.to_path_buf())
|
||||
})?
|
||||
} else {
|
||||
path
|
||||
path.to_path_buf()
|
||||
};
|
||||
relative
|
||||
.components()
|
||||
|
|
@ -168,7 +181,7 @@ impl ApprovedRoot {
|
|||
std::path::Component::Normal(_) | std::path::Component::CurDir
|
||||
)
|
||||
})
|
||||
.then(|| relative.to_path_buf())
|
||||
.then_some(relative)
|
||||
}
|
||||
|
||||
pub fn resolve_regular_file(&self, path: &Path) -> Option<(PathBuf, Metadata)> {
|
||||
|
|
|
|||
|
|
@ -838,14 +838,18 @@ fn state_database_probes_have_a_supported_generation_ceiling() {
|
|||
fs::write(&boundary, "").unwrap();
|
||||
fs::write(&beyond, "").unwrap();
|
||||
let approved_root = ApprovedRoot::new(root.path()).unwrap();
|
||||
let root_path = approved_root.path();
|
||||
assert_eq!(
|
||||
state_databases(&approved_root).collect::<Vec<_>>(),
|
||||
vec![boundary.clone(), root.path().join("state_2.sqlite")]
|
||||
vec![
|
||||
root_path.join(format!("state_{MAX_STATE_DB_GENERATION}.sqlite")),
|
||||
root_path.join("state_2.sqlite"),
|
||||
]
|
||||
);
|
||||
fs::remove_file(boundary).unwrap();
|
||||
fs::remove_file(root_path.join(format!("state_{MAX_STATE_DB_GENERATION}.sqlite"))).unwrap();
|
||||
assert_eq!(
|
||||
state_databases(&approved_root).collect::<Vec<_>>(),
|
||||
vec![root.path().join("state_2.sqlite")]
|
||||
vec![root_path.join("state_2.sqlite")]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3722,7 +3722,6 @@ pub async fn connect_local_workspace(
|
|||
status_config: crate::status_config::StatusConfig,
|
||||
upload_queue_enabled: bool,
|
||||
project_lsp_trusted: bool,
|
||||
ready_file: Option<std::path::PathBuf>,
|
||||
diag: Option<DiagHandle>,
|
||||
require_explicit_toolset: bool,
|
||||
confine_fs_to_workspace_root: bool,
|
||||
|
|
@ -3752,7 +3751,6 @@ pub async fn connect_local_workspace(
|
|||
server_id,
|
||||
alpha_test_key,
|
||||
allow_insecure_ws,
|
||||
ready_file,
|
||||
diag,
|
||||
};
|
||||
let tool_config = xai_grok_agent::workspace_grok_build_toolset();
|
||||
|
|
@ -3836,10 +3834,10 @@ pub async fn connect_local_workspace(
|
|||
/// 2. `<grok_home>/workspace`, where `<grok_home>` honours `$GROK_HOME` and
|
||||
/// otherwise falls back to `~/.grok` (see [`xai_grok_config::grok_home`]).
|
||||
pub fn resolve_workspace_home() -> std::path::PathBuf {
|
||||
if let Ok(p) = std::env::var("GROK_WORKSPACE_HOME") {
|
||||
if !p.trim().is_empty() {
|
||||
return std::path::PathBuf::from(p);
|
||||
}
|
||||
if let Ok(p) = std::env::var("GROK_WORKSPACE_HOME")
|
||||
&& !p.trim().is_empty()
|
||||
{
|
||||
return std::path::PathBuf::from(p);
|
||||
}
|
||||
xai_grok_config::grok_home().join("workspace")
|
||||
}
|
||||
|
|
@ -4300,7 +4298,7 @@ impl WorkspaceHandle {
|
|||
)
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl WorkspaceHandle {
|
||||
fn test_config(
|
||||
root_cwd: std::path::PathBuf,
|
||||
|
|
@ -7506,7 +7504,6 @@ pub(crate) mod tests {
|
|||
server_id: Some("server-1".to_string()),
|
||||
alpha_test_key: None,
|
||||
allow_insecure_ws: true,
|
||||
ready_file: None,
|
||||
diag: None,
|
||||
};
|
||||
let config = WorkspaceConfig::new_for_proxy(
|
||||
|
|
|
|||
|
|
@ -72,55 +72,11 @@ pub struct HubConfig {
|
|||
pub alpha_test_key: Option<String>,
|
||||
/// Permit a plaintext `ws://` server on a non-loopback host (mesh-secured).
|
||||
pub allow_insecure_ws: bool,
|
||||
/// When set, the ready file tracks hub readiness for the sandbox reconnect
|
||||
/// gate: written on initial connect (hello / server registered), removed on
|
||||
/// disconnect, rewritten only after reconnect **serve replay settles**
|
||||
/// (not merely socket-up). Presence means the tool-server is registered and
|
||||
/// prior sessions have been re-served when applicable. `None` = unmanaged.
|
||||
pub ready_file: Option<std::path::PathBuf>,
|
||||
/// Diagnostics-server state handle, driven from the same lifecycle points
|
||||
/// as the ready file. `None` = no diagnostics server (embedded/local use).
|
||||
/// Diagnostics-server state handle driving the `/ready` state from the
|
||||
/// connection lifecycle. `None` = no diagnostics server (embedded/local
|
||||
/// use).
|
||||
pub diag: Option<DiagHandle>,
|
||||
}
|
||||
/// Write the workspace-server ready file (pid as contents). Presence means the
|
||||
/// tool-server is hub-ready for the sandbox gate (initial hello, or reconnect
|
||||
/// after serve replay settled); failures are logged, not fatal.
|
||||
fn write_ready_file(path: &std::path::Path) {
|
||||
if let Err(e) = std::fs::write(path, std::process::id().to_string()) {
|
||||
tracing::warn!(
|
||||
path = % path.display(), error = % e,
|
||||
"failed to write workspace-server ready file"
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Publishes hub readiness to the ready file and the diagnostics server from
|
||||
/// the same lifecycle transitions, so the two protocols cannot disagree while
|
||||
/// a diagnostics handle is configured (its shutdown latch gates both).
|
||||
struct ReadyPublisher {
|
||||
ready_file: Option<std::path::PathBuf>,
|
||||
diag: Option<DiagHandle>,
|
||||
}
|
||||
impl ReadyPublisher {
|
||||
fn connected(&self) {
|
||||
if self.diag.as_ref().is_some_and(DiagHandle::is_shutting_down) {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = &self.ready_file {
|
||||
write_ready_file(path);
|
||||
}
|
||||
if let Some(diag) = &self.diag {
|
||||
diag.set_connected();
|
||||
}
|
||||
}
|
||||
fn disconnected(&self) {
|
||||
if let Some(path) = &self.ready_file {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
if let Some(diag) = &self.diag {
|
||||
diag.set_disconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for HubConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HubConfig")
|
||||
|
|
@ -277,17 +233,13 @@ impl HubHandle {
|
|||
}
|
||||
});
|
||||
}
|
||||
if config.ready_file.is_some() || config.diag.is_some() {
|
||||
let publisher = Arc::new(ReadyPublisher {
|
||||
ready_file: config.ready_file.clone(),
|
||||
diag: config.diag.clone(),
|
||||
});
|
||||
let on_connect = Arc::clone(&publisher);
|
||||
let on_disconnect = Arc::clone(&publisher);
|
||||
if let Some(diag) = config.diag.clone() {
|
||||
let on_connect = diag.clone();
|
||||
let on_disconnect = diag.clone();
|
||||
server_builder = server_builder
|
||||
.on_connect(move || on_connect.connected())
|
||||
.on_disconnect(move || on_disconnect.disconnected())
|
||||
.on_reconnect_settled(move || publisher.connected());
|
||||
.on_connect(move || on_connect.set_connected())
|
||||
.on_disconnect(move || on_disconnect.set_disconnected())
|
||||
.on_reconnect_settled(move || diag.set_connected());
|
||||
}
|
||||
if let Some(ref id) = config.server_id {
|
||||
server_builder = server_builder.server_id(parse_server_id(id)?);
|
||||
|
|
|
|||
|
|
@ -267,8 +267,7 @@ impl WorkspaceHandle {
|
|||
self.shared
|
||||
.activity_tracker
|
||||
.turn_started(session_id, turn_number);
|
||||
let handle = None;
|
||||
handle
|
||||
None
|
||||
}
|
||||
TurnBoundary::End {
|
||||
prompt_index: Some(idx),
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ fn build_web_fetch_config() -> xai_grok_tools::implementations::grok_build::web_
|
|||
fn default_web_search_model() -> String {
|
||||
std::env::var("GROK_WEB_SEARCH_MODEL").unwrap_or_else(|_| "grok-4.20-multi-agent".to_string())
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test_support {
|
||||
use crate::config::SessionContextFactory;
|
||||
use std::collections::HashMap;
|
||||
|
|
|
|||
|
|
@ -1514,7 +1514,7 @@ impl WorkspaceOps {
|
|||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl WorkspaceOps {
|
||||
/// Test variant backed by a temp dir.
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue