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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue