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:
parent
8adf9013a0
commit
98c3b2438a
225 changed files with 18836 additions and 7156 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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(¶ms)
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue