Synced from monorepo

Changes:
- Gate session-lifecycle heap steady state with a dhat soak
- Unbreak merge lifecycle e2e after default model → grok-4.5
- Scan home-scope rules dirs at <root>/rules
- Complete text-input paste and terminal parity
- Gate project roles and personas
- Use canonical editing in dialogs
- Use canonical editing in search bars
- Reject ambiguous MCP tool IDs
- Harden Git operands for plugins
- Simplify queue drain API
- Pass RFC 9207 iss through MCP OAuth token exchange
- Show leader roster when local agents map is empty
- Use canonical editing in Persona views
- Remove marketplace default-skills auto-install and purge old installs
- Use canonical editing in extension forms
- Add canonical dashboard text editing
- Use canonical editing in settings
- Add /summarize as a /recap alias
- Restore previous agent when exiting dashboard
- Use tool_choice auto for compaction
- Settings toggle for snap-prompt-to-top on send
- Update default models to grok-4.5
- Source login shell once for local bash (env + alias/function snapshot)
- Template hardcoded param names in server-native tool descriptions
- Fix System-Reminder XML tag injection in CLAUDE.md via agents_md
- Fix remote workspace-server hardcoding LSP trust (repo code execution risk)
- Clear orphaned tool-call updates at turn end
- Suppress task wake after cancel
- Send x-grok-client-identifier on direct API tool calls
- Harden dashboard peek lease transitions
- Host /btw side panel in live region (minimal mode)
- Bound scroll presentation latency
- Highlight multi-line constructs correctly in diffs and the file viewer
- Block web_fetch non-public IPs; local opt-in is explicit-host only
- Seed coding_data_retention_opt_out=false for OAuth e2es in pty-harness
- Follow up clipboard delivery feedback
- Use canonical editing in pickers
- Route TextArea through canonical editor
- Persistent "watching" status row; quieter turn markers
- Gate sensitive edit targets
- Expose agent registry counts and gate session churn on them
- Default coding data sharing to opt-out until server preference applies
- Wire chat attachment ids through gateway prompts
- On auth refresh failure, issue retry
- Forward preview provenance and computer lifecycle state
- Document independent privacy controls and scope /privacy output
- Strip SamplingError Display prefix on rate-limit UI copy
- Stop dumping Cloudflare HTML into Retry failed
- Disable in-place prompt edit (scroll jank on enter)
- Strip forced ANSI color from gh pr view JSON
- Plumb bash tool description onto ToolUsageCard wire
This commit is contained in:
grokkybara[bot] 2026-07-18 19:48:28 +01:00
commit 7cfcb20d2b
292 changed files with 23315 additions and 9209 deletions

View file

@ -482,7 +482,7 @@ async fn build_auth(
team_role: None,
user_blocked_reason: None,
team_blocked_reasons: vec![],
coding_data_retention_opt_out: false,
coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
has_grok_code_access: None,
refresh_token: tokens.refresh_token.clone(),
expires_at: tokens.expires_in.map(|s| now + Duration::seconds(s)),

View file

@ -72,7 +72,7 @@ pub(crate) fn parse_output(output: &std::process::Output) -> anyhow::Result<Grok
organization_role: None,
user_blocked_reason: None,
team_blocked_reasons: vec![],
coding_data_retention_opt_out: false,
coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
has_grok_code_access: None,
refresh_token,
expires_at,

View file

@ -11,7 +11,7 @@ pub struct GateInfo {
}
/// Typed auth metadata passed from the shell to the pager via ACP.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthMeta {
#[serde(default)]
pub email: Option<String>,
@ -26,7 +26,8 @@ pub struct AuthMeta {
pub is_zdr: bool,
#[serde(default)]
pub team_role: Option<String>,
#[serde(default)]
/// Defaults to opted-out (safer) until auth meta is populated.
#[serde(default = "crate::auth::default_coding_data_retention_opt_out")]
pub coding_data_retention_opt_out: bool,
#[serde(default)]
pub show_resolved_model: Option<bool>,
@ -38,3 +39,20 @@ pub struct AuthMeta {
#[serde(default)]
pub subscription_tier: Option<String>,
}
impl Default for AuthMeta {
fn default() -> Self {
Self {
email: None,
auth_mode: None,
team_id: None,
team_name: None,
is_zdr: false,
team_role: None,
coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
show_resolved_model: None,
gate: None,
subscription_tier: None,
}
}
}

View file

@ -37,7 +37,9 @@ pub use error::{AuthError, RefreshTokenError, RefreshTokenFailedReason};
pub use manager::{AuthManager, shared_api_key_provider};
pub use meta::{AuthMeta, GateInfo};
pub use model::{AuthMode, GrokAuth, lookup_auth};
pub(crate) use model::{TOKEN_TTL, UserInfo, is_expired, token_suffix};
pub(crate) use model::{
TOKEN_TTL, UserInfo, default_coding_data_retention_opt_out, is_expired, token_suffix,
};
pub(crate) use refresh::DiagnosticUploader;
pub use storage::{
clear_api_key, read_api_key, read_auth_json, read_token_by_scope, store_api_key,

View file

@ -16,6 +16,13 @@ pub const API_KEY_SCOPE: &str = "xai::api_key";
const BLOCKED_REASON_NO_LOGS: &str = "BLOCKED_REASON_NO_LOGS";
const BLOCKED_REASON_NO_LOGS_MODERATED: &str = "BLOCKED_REASON_NO_LOGS_MODERATED";
/// Fresh-credential / missing-field default: opted out until the user or
/// server enrichment opts in. Single source for `GrokAuth`, `AuthMeta`, and
/// every login-path constructor so the sides cannot drift.
pub(crate) fn default_coding_data_retention_opt_out() -> bool {
true
}
/// Token provenance (debugging/auth.json only -- no code branches on this).
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
@ -69,7 +76,9 @@ pub struct GrokAuth {
pub user_blocked_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub team_blocked_reasons: Vec<String>,
#[serde(default)]
/// Defaults to `true` (opted out) for safer consumer privacy until the
/// user explicitly shares or server enrichment sets the team preference.
#[serde(default = "default_coding_data_retention_opt_out")]
pub coding_data_retention_opt_out: bool,
/// Deprecated. Kept for deserializing existing auth.json files.
@ -218,7 +227,7 @@ impl Default for GrokAuth {
organization_role: None,
user_blocked_reason: None,
team_blocked_reasons: vec![],
coding_data_retention_opt_out: false,
coding_data_retention_opt_out: default_coding_data_retention_opt_out(),
has_grok_code_access: None,
refresh_token: None,
expires_at: None,
@ -239,6 +248,9 @@ impl GrokAuth {
Self {
key: "test-key".into(),
user_id: "test-user".into(),
// Tests that exercise collection gates need sharing enabled by
// default; opt out explicitly when asserting the privacy path.
coding_data_retention_opt_out: false,
..Default::default()
}
}
@ -486,4 +498,23 @@ mod tests {
let info: UserInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.subscription_tier.as_deref(), Some(""));
}
/// Pre-default auth.json (no coding_data_retention_opt_out key) must
/// deserialize as opted-out, not the old fail-open false.
#[test]
fn missing_coding_data_retention_opt_out_deserializes_opted_out() {
let json = r#"{
"key": "k",
"auth_mode": "oidc",
"create_time": "2020-01-01T00:00:00Z",
"user_id": "u"
}"#;
let auth: GrokAuth = serde_json::from_str(json).unwrap();
assert!(
auth.coding_data_retention_opt_out,
"missing field must default to opted-out"
);
assert!(default_coding_data_retention_opt_out());
assert!(GrokAuth::default().coding_data_retention_opt_out);
}
}

View file

@ -689,7 +689,7 @@ pub(super) async fn validate_and_extract_user_info(
organization_role: None,
user_blocked_reason: None,
team_blocked_reasons: vec![],
coding_data_retention_opt_out: false,
coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
})
}
pub(super) async fn extract_user_info(
@ -720,7 +720,7 @@ pub(super) async fn extract_user_info(
organization_role: None,
user_blocked_reason: None,
team_blocked_reasons: vec![],
coding_data_retention_opt_out: false,
coding_data_retention_opt_out: crate::auth::default_coding_data_retention_opt_out(),
});
}
let token = id_token.ok_or_else(|| anyhow::Error::new(OidcError::MissingIdToken))?;

View file

@ -61,6 +61,9 @@ impl TokenRefresher for ExternalBinaryRefresher {
.await
{
Err(_elapsed) => {
// Transient: a hard-expired access token after idle must still
// allow 401 / pre-flight retry. Mapping timeout to permanent
// failure poisoned recovery for PERMANENT_FAILURE_TTL.
tracing::warn!(
timeout_ms,
"auth: external binary refresh timed out (thread leaked)"
@ -70,7 +73,7 @@ impl TokenRefresher for ExternalBinaryRefresher {
None,
Some(serde_json::json!({ "timeout_ms": timeout_ms })),
);
self.record_failure(format!("external binary timed out after {timeout_ms}ms"))
RefreshOutcome::transient(format!("external binary timed out after {timeout_ms}ms"))
}
Ok(Ok(Some(auth))) => {
crate::unified_log::info("auth: external binary refresh succeeded", None, None);
@ -131,10 +134,8 @@ mod tests {
}
}
/// A binary that outlives the (test-shortened) timeout hits the `Elapsed`
/// arm and maps to the same non-sticky `Other` permanent failure.
#[tokio::test]
async fn external_binary_timeout_is_non_sticky_permanent() {
async fn external_binary_timeout_is_transient() {
struct SlowRunner;
impl ExternalCommandRunner for SlowRunner {
fn run_external_command(&self, _command: &str) -> Option<GrokAuth> {
@ -145,14 +146,13 @@ mod tests {
let refresher = ExternalBinaryRefresher::new(Arc::new(SlowRunner), "auth-binary".into())
.with_timeout(std::time::Duration::from_millis(5));
match refresher.refresh(RefreshReason::ServerRejected).await {
RefreshOutcome::PermanentFailure { error, .. } => {
assert_eq!(error.reason, RefreshTokenFailedReason::Other);
RefreshOutcome::TransientFailure { message } => {
assert!(
!error.reason.is_sticky(),
"timeout must age out, not strand"
message.contains("timed out"),
"timeout message must be greppable, got {message}"
);
}
other => panic!("a timed-out binary must be a permanent Other failure, got {other:?}"),
other => panic!("a timed-out binary must be TransientFailure, got {other:?}"),
}
}