Synced from monorepo

Changes:
- Classify clipboard delivery confidence
- Add durable session update append
- Scope the xAI session bearer to first-party memory embedding endpoints
- Persist subagent outputs to disk and bound long-lived agent state
- Add MiniSweAgent:bash for mini-swe-agent parity
- Revert taking local sessions off the persistent shell
- Contextual tip recommending grok wrap on SSH sessions
- Voice STT bearer from model BYOK env_key/api_key
- Define exact website policies for sandbox
- Gate unsafe shell environments
- Shared pin hoist; single require_sha gate for marketplace plugins
- Server-signed is-managed claim (closes sidecar-removal downgrade)
- Optional require_sha pin for remote plugin installs
- Show session title and last exchange in the exit resume hint
- Gate shell output redirects
- Warn when fail_closed is present but not a boolean
- Add canonical text editing core (ratatui-textarea)
- Keep execution state out of goal scratch
- Add acknowledged persistence primitives
- Inherit child network restrictions in sandbox
- Fail closed when hook matchers fail to recompile
- Add MCP setup preferences for plugin MCPs
- Gate sourced shell scripts
- Gate file-typed project hooks
- grok wrap: restore terminal modes on child death
- Harden owner-only permissions on auth and MCP credentials
- Create crash dump files with owner-only permissions
- Write the agent_id cache owner-only (0600)
- SessionMetrics mode skips Mixpanel profile sync
- Dashboard: slim live-tail peek
- Yank full queued prompt text, not (+N lines)
- Defeat clock-rollback on the signed managed-config cache
- Stop early session/cancel from overtaking the prompt and wedging the turn slot
- Self-heal a diverged agent entrypoint on startup
- Add matched inference expectations in test-support
- Add AuthSingleFlight cancel/successor gap tests
- Remove consumer from external OTEL allowlist and pin scrub coverage
- Enable /copy in minimal mode
- Surface capacity and API-key detail on 429 errors
- Single-flight interactive auth
- Fix PageUp/PageDown skipping lines behind sticky prompt header
This commit is contained in:
grokkybara[bot] 2026-07-17 14:19:50 +01:00
commit 98c3b2438a
225 changed files with 18836 additions and 7156 deletions

View file

@ -7,7 +7,7 @@ use super::actions::{PermissionModePersist, SubagentKillOutcome, TaskResult};
use super::agent::AgentId;
use crate::unified_log as ulog;
use xai_grok_shell::sampling::error::{
RATE_LIMITED_ERROR_CODE, rate_limited_user_message,
RATE_LIMITED_ERROR_CODE, error_detail_from_data, format_rate_limited_user_message,
};
use xai_grok_shell::session::ExtMethodResult;
/// Typed progress message for session restore.
@ -81,18 +81,17 @@ pub(super) async fn fetch_plugin_cta_mcps(
}
}
/// Convert an ACP error to a user-friendly string for display.
/// Rate-limit errors get auth-aware copy instead of the raw server error.
/// Rate-limit errors: free-usage paywall, else server detail (with API-key
/// rewrite when the body pushes personal SuperGrok), else auth-aware fallback
/// (see [`format_rate_limited_user_message`]).
/// All other errors are sanitized to remove internal service names and jargon.
pub(super) fn format_acp_error(err: &acp::Error, is_api_key_auth: bool) -> String {
if i32::from(err.code) == RATE_LIMITED_ERROR_CODE {
if super::dispatch::acp_error_is_free_usage_exhausted(err) {
return super::dispatch::FREE_USAGE_USER_MESSAGE.into();
}
return rate_limited_user_message(is_api_key_auth).into();
let detail = err.data.as_ref().and_then(error_detail_from_data);
return format_rate_limited_user_message(detail.as_deref(), is_api_key_auth);
}
if err.code == acp::ErrorCode::InvalidParams && let Some(data) = &err.data
&& let Some(msg) = xai_grok_shell::sampling::error::error_detail_from_data(data)
&& !msg.is_empty()
&& let Some(msg) = error_detail_from_data(data) && !msg.is_empty()
{
return msg;
}
@ -339,6 +338,9 @@ pub(crate) struct EffectMeta {
/// install this into `AppView.auth_state` if the current auth state
/// still matches the sequence.
pub auth_abort_handle: Option<(u64, tokio::task::AbortHandle)>,
/// Auth URL poll abort handle + request sequence (installed on
/// `AppView.auth_url_poll_handle` when the seq still matches).
pub auth_url_poll_handle: Option<(u64, tokio::task::AbortHandle)>,
}
/// Extract the first user prompt text from a session's `chat_history.jsonl`.
///
@ -639,6 +641,23 @@ pub(super) async fn send_logout(tx: &AcpAgentTx) {
tracing::warn!(error = % e, "logout failed");
}
}
/// Best-effort `x.ai/auth/cancel`: stops the shell's device/loopback wait so a
/// later login is single-flight. Errors are ignored — UI already left
/// `Authenticating`. `request_seq` scopes the cancel to the abandoned attempt.
pub(super) async fn send_auth_cancel(tx: &AcpAgentTx, request_seq: u64) -> TaskResult {
let req = acp::ExtRequest::new(
"x.ai/auth/cancel",
serde_json::value::to_raw_value(
&serde_json::json!({ "request_seq" : request_seq }),
)
.expect("serialize auth/cancel params")
.into(),
);
if let Err(e) = acp_send(req, tx).await {
tracing::debug!(error = % e, "auth cancel ext request failed (ignored)");
}
TaskResult::AuthCancelComplete
}
pub(super) async fn send_check_subscription(
tx: &AcpAgentTx,
verify: Option<u64>,
@ -713,7 +732,9 @@ pub(super) async fn send_authenticate(
use_oauth: bool,
force_interactive: bool,
) -> TaskResult {
let mut meta = serde_json::json!({ "use_oauth" : use_oauth });
let mut meta = serde_json::json!(
{ "use_oauth" : use_oauth, "request_seq" : request_seq, }
);
if force_interactive {
meta["force_interactive"] = serde_json::json!(true);
}
@ -839,6 +860,14 @@ pub(crate) async fn persist_setting(
.await
.map_err(|e| e.to_string())
}
"contextual_hints.ssh_wrap" => {
let SettingValue::Bool(b) = value else {
return Err(kind_mismatch("contextual_hints.ssh_wrap", "Bool", &value));
};
xai_grok_shell::util::config::set_contextual_hint_ssh_wrap(b)
.await
.map_err(|e| e.to_string())
}
"theme" => {
let SettingValue::Enum(s) = value else {
return Err(kind_mismatch("theme", "Enum", &value));

View file

@ -81,6 +81,10 @@ pub(crate) fn execute(
TaskResult::LogoutComplete
});
}
Effect::CancelAuth { request_seq } => {
let tx = acp_tx.clone();
tasks.spawn(async move { send_auth_cancel(&tx, request_seq).await });
}
Effect::CheckSubscription { verify } => {
let tx = acp_tx.clone();
tasks.spawn(async move { send_check_subscription(&tx, verify).await });
@ -1900,7 +1904,7 @@ pub(crate) fn execute(
}
Effect::PollAuthUrl { request_seq } => {
let tx = acp_tx.clone();
tasks
let abort_handle = tasks
.spawn(async move {
let mut auth_url: Option<String> = None;
let mut external = false;
@ -1944,6 +1948,7 @@ pub(crate) fn execute(
mode,
}
});
meta.auth_url_poll_handle = Some((request_seq, abort_handle));
}
Effect::SubmitAuthCode { request_seq, code } => {
let tx = acp_tx.clone();
@ -2043,7 +2048,24 @@ pub(crate) fn execute(
.and_then(|s| s.as_str())
.unwrap_or("unknown");
if status == "authenticated" {
Ok(())
Ok(
crate::app::actions::McpAuthTriggerOutcome::Authenticated,
)
} else if status == "setup_required" {
let setup = result_obj
.and_then(|r| r.get("setup"))
.cloned()
.and_then(|value| {
serde_json::from_value::<
crate::views::mcps_modal::McpSetupConfig,
>(value)
.ok()
})
.ok_or_else(|| "setup required".to_string());
setup
.map(
crate::app::actions::McpAuthTriggerOutcome::SetupRequired,
)
} else {
let detail = result_obj
.and_then(|r| r.get("error"))
@ -2066,6 +2088,51 @@ pub(crate) fn execute(
}
});
}
Effect::McpSetupSubmit { agent_id, session_id, server_name, values } => {
let tx = acp_tx.clone();
tasks
.spawn(async move {
let params = serde_json::json!(
{ "sessionId" : session_id.0.to_string(), "serverName" :
server_name, "values" : values, }
);
let req = acp::ExtRequest::new(
"x.ai/mcp/setup",
serde_json::value::to_raw_value(&params)
.expect("serialize mcp/setup params")
.into(),
);
let result = match acp_send(req, &tx).await {
Ok(resp) => {
let wrapper: serde_json::Value = serde_json::from_str(
resp.0.get(),
)
.unwrap_or_default();
let result_obj = wrapper.get("result");
if result_obj
.and_then(|r| r.get("ok"))
.and_then(|ok| ok.as_bool())
.unwrap_or(false)
{
Ok(())
} else {
let detail = result_obj
.and_then(|r| r.get("error"))
.and_then(|e| e.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "setup failed".to_string());
Err(detail)
}
}
Err(e) => Err(sanitize_user_error(&format!("setup failed: {e}"))),
};
TaskResult::McpSetupSubmitDone {
agent_id,
server_name,
result,
}
});
}
Effect::FetchHooksList { agent_id, session_id } => {
let tx = acp_tx.clone();
tasks
@ -4180,17 +4247,9 @@ fn format_session_info(
.as_deref()
.map(|b| format!("\n API Backend: {b}"))
.unwrap_or_default();
let sandbox_line = match xai_grok_sandbox::profile_name() {
Some(profile) => {
let net = if xai_grok_sandbox::should_restrict_child_network() {
" (network: restricted)"
} else {
""
};
format!("\n Sandbox: {profile}{net}")
}
None => String::new(),
};
let sandbox_line = xai_grok_sandbox::profile_name()
.map(|profile| format!("\n Sandbox: {profile}"))
.unwrap_or_default();
let turn_line = format!("\n Turn: {}", info.data.turn_index);
let conversation_line = info
.data

View file

@ -17,14 +17,31 @@ fn format_acp_error_reads_detail_from_wrapped_data() {
assert_eq!(format_acp_error(& wrapped, false), "model does not support tools");
}
#[test]
fn format_acp_error_rate_limit_is_auth_aware() {
fn format_acp_error_rate_limit_surfaces_detail_or_fallback() {
use xai_grok_shell::sampling::error::{
RATE_LIMITED_ERROR_CODE, RATE_LIMITED_USER_MESSAGE_API_KEY,
RATE_LIMITED_USER_MESSAGE_OAUTH,
FREE_USAGE_USER_MESSAGE, RATE_LIMITED_ERROR_CODE,
RATE_LIMITED_USER_MESSAGE_API_KEY, RATE_LIMITED_USER_MESSAGE_OAUTH,
};
let err = acp::Error::new(RATE_LIMITED_ERROR_CODE, "Rate limited").data("slow down");
assert_eq!(format_acp_error(& err, false), RATE_LIMITED_USER_MESSAGE_OAUTH);
assert_eq!(format_acp_error(& err, true), RATE_LIMITED_USER_MESSAGE_API_KEY);
let capacity = acp::Error::new(RATE_LIMITED_ERROR_CODE, "Rate limited")
.data(
"The service is temporarily at capacity. Please retry your request shortly.",
);
let cap = "The service is temporarily at capacity. Please retry your request shortly.";
assert_eq!(format_acp_error(& capacity, false), cap);
assert_eq!(format_acp_error(& capacity, true), cap);
let rpm = acp::Error::new(RATE_LIMITED_ERROR_CODE, "Rate limited")
.data(
"You are sending requests too quickly. Please slow down, or upgrade to a Grok subscription for higher limits: https://grok.com/supergrok",
);
assert!(format_acp_error(& rpm, false).contains("grok.com/supergrok"));
assert_eq!(format_acp_error(& rpm, true), RATE_LIMITED_USER_MESSAGE_API_KEY);
let empty = acp::Error::new(RATE_LIMITED_ERROR_CODE, "Rate limited");
assert_eq!(format_acp_error(& empty, false), RATE_LIMITED_USER_MESSAGE_OAUTH);
assert_eq!(format_acp_error(& empty, true), RATE_LIMITED_USER_MESSAGE_API_KEY);
let free = acp::Error::new(RATE_LIMITED_ERROR_CODE, "Rate limited")
.data("subscription:free-usage-exhausted: You have used all your free usage.");
assert_eq!(format_acp_error(& free, false), FREE_USAGE_USER_MESSAGE);
assert_eq!(format_acp_error(& free, true), FREE_USAGE_USER_MESSAGE);
}
/// Non-empty token ranges ride the wire block meta as `skillTokenRanges`
/// byte pairs; the text itself is untouched.