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:
grokkybara[bot] 2026-07-16 20:27:30 +01:00
commit 8adf9013a0
117 changed files with 16998 additions and 14540 deletions

View file

@ -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() {

View file

@ -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.
///

View file

@ -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)]

View file

@ -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)> {

View file

@ -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")]
);
}

View file

@ -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(

View file

@ -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)?);

View file

@ -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),

View file

@ -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;

View file

@ -1514,7 +1514,7 @@ impl WorkspaceOps {
}
}
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl WorkspaceOps {
/// Test variant backed by a temp dir.
///