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

@ -21,16 +21,7 @@ pub const GROK_CHAT_MODE_ENV: &str = "GROK_CHAT_MODE";
/// True when the process is a gateway light-frontend (`--chat`) agent.
/// Hard-off in release builds so it can't be enabled via env.
pub fn process_chat_mode_enabled() -> bool {
if true {
return false;
}
match std::env::var(GROK_CHAT_MODE_ENV) {
Ok(v) => {
let v = v.trim();
!v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false")
}
Err(_) => false,
}
false
}
#[derive(Clone)]
struct CachedModes {

View file

@ -115,10 +115,10 @@ impl EnvKeys {
mut getenv: impl FnMut(&str) -> Option<String>,
) -> Option<String> {
for name in self.names() {
if let Some(value) = getenv(name) {
if !value.trim().is_empty() {
return Some(value);
}
if let Some(value) = getenv(name)
&& !value.trim().is_empty()
{
return Some(value);
}
}
None
@ -285,7 +285,8 @@ impl EndpointsConfig {
}
/// Layer the `[endpoints]` table from `config` over the env/default base.
/// No field is derived from another — defaulting is done by the resolvers.
pub(crate) fn from_config_value(config: &toml::Value) -> Self {
/// `pub`: the pager resolves the voice STT base through this same path.
pub fn from_config_value(config: &toml::Value) -> Self {
let default = Self::default();
let external_otel_master_switch = default.external_otel_master_switch;
let mut base = match toml::Value::try_from(default) {

View file

@ -736,8 +736,10 @@ pub struct MvpAgent {
/// Context for managing background copy operations (e.g., copying ignored files)
pub(crate) background_copy_context: BackgroundCopyContext,
/// LEADER-SAFE(per-session): keyed by SessionId, no cross-session iteration.
/// Released by `remove_session`.
pub(crate) session_turn_numbers: RefCell<HashMap<acp::SessionId, u64>>,
/// LEADER-SAFE(per-session): keyed by SessionId, no cross-session iteration.
/// Released by `remove_session`.
permission_event_receivers: RefCell<
HashMap<acp::SessionId, tokio::sync::mpsc::UnboundedReceiver<PermissionEvent>>,
>,
@ -772,7 +774,7 @@ pub struct MvpAgent {
/// transiently degraded when a reconnect replays `session/load` (e.g.
/// fetch still in flight after a leader restart), so the prompt path
/// re-checks and self-heals — or (b) the user explicitly switches
/// models via `set_session_model`.
/// models via `set_session_model`. Released by `remove_session`.
model_unavailable_sessions: RefCell<std::collections::HashMap<String, acp::ModelId>>,
/// Unified sender for all subagent coordinator events.
/// LEADER-SAFE(shared): channel is multi-producer, coordinator drains.
@ -2286,11 +2288,8 @@ async fn handle_synthetic_turn_trace(
model,
)
};
let trace_context = {
let this = agent_ref.get();
let ctx = this.get_trace_context(&info, turn_number).await;
ctx
};
let this = agent_ref.get();
let trace_context = this.get_trace_context(&info, turn_number).await;
let Some(ctx) = trace_context else {
tracing::info!(
session_id = % request.session_id.0, prompt_id = % request.prompt_id,

View file

@ -30,20 +30,26 @@ impl MvpAgent {
});
}
}
/// Remove a session and its thread handle **without** finalizing the cloud
/// replica.
///
/// Used for dead-actor reaping and idle-unload: the conversation stays
/// resumable on disk, so it must NOT be marked "done" upstream. Genuine
/// terminal closes go through [`MvpAgent::close_session_explicit`]. Also
/// drops the `session_live_state` entry so that map stays bounded.
/// Remove a session and its thread handle without finalizing the cloud
/// replica; the conversation stays resumable on disk. Reached by
/// dead-actor reaping and the terminal close and delete paths. Idle
/// unload does not route here: `handle_evict_sessions` removes its
/// handle inline and keeps the thread for reconnect.
pub(crate) fn remove_session(&self, id: &acp::SessionId) {
self.sessions.borrow_mut().remove(id);
self.prompt_intake_locks.borrow_mut().remove(id);
self.session_threads.borrow_mut().remove(id);
self.session_index_claims.borrow_mut().remove(id);
self.require_gateway_sessions.borrow_mut().remove(id);
self.model_unavailable_sessions
.borrow_mut()
.remove(id.0.as_ref());
self.permission_event_receivers.borrow_mut().remove(id);
self.session_turn_numbers.borrow_mut().remove(id);
self.session_live_state.borrow_mut().remove(id);
if let Some(ops) = self.workspace_ops.borrow().as_ref() {
ops.end_local_session(id.0.as_ref());
}
}
/// Get-or-create the per-session prompt-intake lock (see
/// [`Self::prompt_intake_locks`]). Cheap clone of the shared `Rc`.

View file

@ -2906,6 +2906,55 @@ fn chat_session_spawn_options_matches_thin_profile() {
"K10 thin profile must use PersistenceHandle::noop()"
);
}
/// `remove_session` releases the workspace binding and drains the
/// per-session side maps. Test agents default to `workspace_ops = None`,
/// so no other test reaches the release.
#[tokio::test]
async fn remove_session_releases_workspace_binding_and_side_maps() {
let agent = build_minimal_agent_for_tests();
let sid = acp::SessionId::new("test-session-workspace-release");
let ops = xai_grok_workspace::WorkspaceOps::for_test();
let toolset =
std::sync::Arc::new(xai_grok_tools::registry::types::FinalizedToolset::empty_for_test());
let toolset_weak = std::sync::Arc::downgrade(&toolset);
ops.bind_local_session(
sid.0.as_ref(),
std::env::temp_dir(),
xai_hunk_tracker::HunkTrackerHandle::noop(),
toolset,
None,
)
.expect("bind_local_session must succeed");
assert!(toolset_weak.upgrade().is_some());
*agent.workspace_ops.borrow_mut() = Some(ops);
agent.model_unavailable_sessions.borrow_mut().insert(
sid.0.to_string(),
acp::ModelId::new(std::sync::Arc::from("gone-model")),
);
agent
.session_turn_numbers
.borrow_mut()
.insert(sid.clone(), 3);
let (_permission_tx, permission_rx) =
tokio::sync::mpsc::unbounded_channel::<xai_grok_workspace::permission::PermissionEvent>();
agent
.permission_event_receivers
.borrow_mut()
.insert(sid.clone(), permission_rx);
agent.remove_session(&sid);
assert!(
toolset_weak.upgrade().is_none(),
"the workspace binding must release the toolset"
);
assert!(
!agent
.model_unavailable_sessions
.borrow()
.contains_key(sid.0.as_ref())
);
assert!(!agent.session_turn_numbers.borrow().contains_key(&sid));
assert!(!agent.permission_event_receivers.borrow().contains_key(&sid));
}
/// Without a bridge, `ext_method` falls through to the unchanged local
/// dispatch (`rewind::handle`), which reports the missing session — proving
/// the routing hook is skipped in local mode.

View file

@ -544,17 +544,15 @@ pub(crate) async fn handle_subagent_request(
forked_conversation,
inherited_prefix_len.unwrap_or(0),
);
if crate::session::is_cursor_user_template(&definition.user_message_template)
&& context_source != InitialContextSource::Resumed && !verbatim_mirror_fork
{} else if context_source != InitialContextSource::Resumed && !verbatim_mirror_fork {
if let Some(ref pi) = effective_runtime.persona_instructions {
let reminder = xai_grok_sampling_types::conversation::ConversationItem::system_reminder(
format!("<system-reminder>\n{pi}\n</system-reminder>"),
);
let insert_at = inherited_prefix_len.min(forked_conversation.len());
forked_conversation.insert(insert_at, reminder);
inherited_prefix_len += 1;
}
if context_source != InitialContextSource::Resumed && !verbatim_mirror_fork
&& let Some(ref pi) = effective_runtime.persona_instructions
{
let reminder = xai_grok_sampling_types::conversation::ConversationItem::system_reminder(
format!("<system-reminder>\n{pi}\n</system-reminder>"),
);
let insert_at = inherited_prefix_len.min(forked_conversation.len());
forked_conversation.insert(insert_at, reminder);
inherited_prefix_len += 1;
}
let effective_source_str = match &context_source {
InitialContextSource::New => "new",
@ -1208,6 +1206,7 @@ pub(crate) async fn handle_subagent_request(
};
if cancel_token.is_cancelled() {
pending_guard.defuse();
ctx.workspace_ops.end_local_session(child_session_id.0.as_ref());
cancel_pending_subagent_at_promote(
request,
&child_handle,
@ -1889,6 +1888,7 @@ pub(crate) async fn handle_subagent_request(
(None, None) => {}
}
let _ = child_handle.cmd_tx.send(SessionCommand::Shutdown);
ctx.workspace_ops.end_local_session(child_session_id.0.as_ref());
let mut disposed_snapshot_ref: Option<String> = None;
let mut worktree_removed = false;
if let Some(ref wt_path) = worktree_path {