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

@ -255,6 +255,10 @@ pub async fn track(event_name: &str, request_id: &str, ctx: &UserContext, mut me
}
/// Sync the user's Mixpanel profile once per init. Fire-and-forget.
///
/// Only runs in [`TelemetryMode::Enabled`]. SessionMetrics mode may emit
/// lifecycle events via [`track`], but must not write Mixpanel people
/// profiles (`engage`).
pub fn sync_profile() {
let lock = TELEMETRY_CLIENT.get_or_init(|| Mutex::new(None));
let client = {
@ -265,6 +269,12 @@ pub fn sync_profile() {
}
};
// The single profile-sync gate: reads the installed client's mode, so every
// caller (and any init race) resolves against what was actually installed.
if !client.mode.is_enabled() {
return;
}
let Some(mixpanel) = client.mixpanel.clone() else {
return;
};
@ -396,6 +406,56 @@ mod tests {
assert_eq!(event_value("grok-workspace-turn"), "turn");
}
/// SessionMetrics must not attempt Mixpanel profile engage — sync_profile
/// is a no-op unless mode is fully Enabled.
#[test]
fn sync_profile_is_noop_in_session_metrics_mode() {
// No tokio runtime here BY DESIGN: if the gate wrongly falls through,
// sync_profile's tokio::spawn panics and fails this test. Converting
// this to #[tokio::test] would silently turn it into theater.
assert!(
tokio::runtime::Handle::try_current().is_err(),
"this test must run without a tokio runtime"
);
// Clear the global client even if an assert below panics.
struct ClearClient;
impl Drop for ClearClient {
fn drop(&mut self) {
let lock = TELEMETRY_CLIENT.get_or_init(|| Mutex::new(None));
*lock.lock().unwrap_or_else(|err| err.into_inner()) = None;
}
}
let _clear = ClearClient;
// Mixpanel configured, but no events endpoint: the global must never
// carry a live funnel out of this test.
let cfg = TelemetryConfig {
mixpanel_enabled: true,
mixpanel_token: Some("test-token".into()),
events_url: None,
events_api_key: None,
..TelemetryConfig::default()
};
init(
cfg,
TelemetryMode::SessionMetrics,
Some("user-1".into()),
None,
None,
None,
"0.0.0-test".into(),
None,
reqwest::Client::new(),
);
// Explicit call must no-op too (init already invoked it once).
sync_profile();
assert!(
is_session_metrics_enabled(),
"client must be live for session metrics"
);
assert!(!is_enabled(), "product analytics must stay off");
}
/// Names without a known emitter prefix pass through unchanged (preserves
/// the old `unwrap_or(event_name)` fallback).
#[test]

View file

@ -64,6 +64,8 @@ pub enum ContextualTipKind {
SmallScreen,
/// Double-click fold/nav path → tip to enable Word select in settings.
WordSelect,
/// SSH session without `grok wrap` → tip to wrap the ssh command locally.
SshWrap,
}
#[derive(Serialize, Clone, Copy)]
@ -1303,10 +1305,15 @@ pub struct ClipboardCopy {
pub data_control: bool,
pub tmux_ok: bool,
pub osc52_ok: bool,
/// `native_ok || tmux_ok || osc52_ok` where tmux/osc52 are real leg outcomes.
/// Evidence classification: `confirmed` | `unverified` | `failed`.
pub delivery: &'static str,
/// An explicit `grok wrap` OSC 52 sink was active.
pub osc52_sink: bool,
/// The process was inside a container without a display server.
pub container_no_display: bool,
/// Historical boolean projection: true unless `delivery == failed`.
pub reported_success: bool,
/// UX toast branch (route-shaped, not leg-shaped): `copied` | `copied_tmux` |
/// `copied_osc_remote` | `copied_osc_container` | `failed`.
/// Exact UX toast branch selected by the environment policy.
pub toast_kind: &'static str,
pub duration_ms: u64,
}
@ -1814,6 +1821,64 @@ telemetry_event!(
mod tests {
use super::*;
fn terminal_telemetry_fixture() -> TerminalTelemetry {
TerminalTelemetry {
brand: "Unknown".into(),
multiplexer: "none".into(),
is_ssh: true,
is_byobu: false,
term_var: "xterm-256color".into(),
tmux_version: "".into(),
xtversion: "".into(),
host_os: "linux".into(),
display_server: "unknown".into(),
modifier_cmd_fate: "unknown".into(),
modifier_opt_fate: "unknown".into(),
enter_modifier_fate: "unknown".into(),
hyperlink_osc8: "unknown".into(),
hyperlink_skip_reason: "none".into(),
clipboard_route: "native+osc52".into(),
clipboard_native_tool: "arboard".into(),
clipboard_data_control: "n/a".into(),
}
}
#[test]
fn clipboard_copy_serialization_preserves_boolean_and_adds_delivery_evidence() {
for delivery in ["confirmed", "unverified", "failed"] {
let value = serde_json::to_value(ClipboardCopy {
terminal: terminal_telemetry_fixture(),
source: "copy_text",
text_len: 12,
route_native: true,
route_tmux: false,
route_osc52: true,
route_label: "native+osc52".into(),
cli_tools_tried: String::new(),
cli_ok_tools: String::new(),
cli_ok: false,
arboard_ok: false,
data_control: false,
tmux_ok: false,
osc52_ok: true,
delivery,
osc52_sink: false,
container_no_display: false,
reported_success: delivery != "failed",
toast_kind: "unverified_osc_remote",
duration_ms: 1,
})
.unwrap();
assert_eq!(value["delivery"], serde_json::json!(delivery));
assert_eq!(
value["reported_success"],
serde_json::Value::Bool(delivery != "failed")
);
assert_eq!(value["osc52_sink"], serde_json::json!(false));
assert_eq!(value["container_no_display"], serde_json::json!(false));
}
}
#[test]
fn manual_auth_name_and_shape() {
assert_eq!(ManualAuth::NAME, "manual_auth");

View file

@ -702,6 +702,7 @@ fn contextual_tip_kind_label(t: events::ContextualTipKind) -> &'static str {
events::ContextualTipKind::SendNow => "send_now",
events::ContextualTipKind::SmallScreen => "small_screen",
events::ContextualTipKind::WordSelect => "word_select",
events::ContextualTipKind::SshWrap => "ssh_wrap",
}
}

View file

@ -718,6 +718,8 @@ fn contextual_tip_maps_every_tip_and_action() {
(K::SmallScreen, A::Accepted, "small_screen", "accepted"),
(K::WordSelect, A::Shown, "word_select", "shown"),
(K::WordSelect, A::Accepted, "word_select", "accepted"),
(K::SshWrap, A::Shown, "ssh_wrap", "shown"),
(K::SshWrap, A::Accepted, "ssh_wrap", "accepted"),
];
for (tip, action, tip_label, action_label) in cases {
let stream = build(gates_off());

View file

@ -38,6 +38,7 @@ fn load_or_compute_agent_id() -> String {
if let Ok(cached) = std::fs::read_to_string(&cache_path) {
let cached = cached.trim();
if !cached.is_empty() {
tighten_agent_id_cache_perms(&cache_path);
return cached.to_string();
}
}
@ -60,12 +61,81 @@ fn load_or_compute_agent_id() -> String {
};
let id = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, machine_hash.as_bytes()).to_string();
// Save to cache file (best effort, ignore errors)
let _ = std::fs::write(&cache_path, &id);
// Save to cache file with owner-only perms (best effort).
let _ = write_agent_id_cache(&cache_path, &id);
id
}
/// Write `$GROK_HOME/agent_id` as owner-read/write only (Unix 0o600) — it is a
/// stable device identifier and must not be world-readable. Atomic temp+rename,
/// so overwriting a loose-perms cache from an older build never leaves the id
/// in a world-readable file.
fn write_agent_id_cache(path: &std::path::Path, id: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
xai_grok_config::fs_atomic::write_atomically(path, id, Some(0o600))
}
/// Best-effort 0o600 on an existing cache: tightens caches written world-readable
/// by older builds. No-op off Unix or on error (the id itself still loads).
fn tighten_agent_id_cache_perms(path: &std::path::Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
#[cfg(not(unix))]
let _ = path;
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn mode(path: &std::path::Path) -> u32 {
std::fs::metadata(path).expect("meta").permissions().mode() & 0o777
}
#[test]
fn agent_id_cache_written_owner_only() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("agent_id");
write_agent_id_cache(&path, "test-agent-id-value").expect("write");
assert_eq!(mode(&path), 0o600, "agent_id cache must be 0o600");
assert_eq!(
std::fs::read_to_string(&path).expect("read").trim(),
"test-agent-id-value"
);
}
/// Overwriting an existing loose-perms cache (e.g. an old build's empty or
/// torn write) must still land 0600 — mode-at-create alone would keep 0644.
#[test]
fn rewrite_over_loose_perms_cache_lands_owner_only() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("agent_id");
std::fs::write(&path, "").expect("write");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("chmod");
write_agent_id_cache(&path, "fresh-id").expect("rewrite");
assert_eq!(mode(&path), 0o600, "rewrite must not inherit loose perms");
assert_eq!(std::fs::read_to_string(&path).expect("read"), "fresh-id");
}
#[test]
fn older_world_readable_cache_is_tightened() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("agent_id");
std::fs::write(&path, "legacy-id").expect("write");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("chmod");
tighten_agent_id_cache_perms(&path);
assert_eq!(mode(&path), 0o600, "legacy cache must be tightened on read");
assert_eq!(std::fs::read_to_string(&path).expect("read"), "legacy-id");
}
}
/// Returns true when workspace marker env vars (`XAI_ROOT` and `XAI_USER`) are set.
///
/// Used as a coarse local gate for features that require a full workspace

View file

@ -99,10 +99,6 @@ pub(super) static ALLOWED_STRING_KEYS: &[&str] = &[
"status",
"action",
"auth_method",
// auth 401 attribution: fixed enum-ish consumer labels only
// (e.g. "OaiCompatClient.chat_completions_stream"); never user content.
// Key suffix fields stay denied — they are token fingerprints.
"consumer",
"to_mode",
"trigger",
"survey_type",
@ -458,7 +454,6 @@ mod tests {
"status",
"action",
"auth_method",
"consumer",
"to_mode",
"trigger",
"survey_type",
@ -502,7 +497,7 @@ mod tests {
assert_eq!(
ALLOWED_STRING_KEYS, expected,
"ALLOWED_STRING_KEYS changed: adding a key exports a new field — confirm it carries no \
user content, then update this pin."
user content and get telemetry-owner review, then update this pin."
);
}
@ -590,4 +585,52 @@ mod tests {
"secret in allowlisted value not scrubbed: {blob}"
);
}
#[test]
fn allowlisted_path_values_are_still_home_scrubbed() {
// Path keys are allowlisted so the field exports, but home/username
// segments must still collapse — allowlist is not a scrub bypass.
let home = dirs::home_dir().expect("home dir for path-scrub test");
let home_str = home.to_string_lossy();
// Skip if the home path is too short/generic for the scrubber to match.
if home_str.len() < 4 {
return;
}
let full = format!("{home_str}/secret-project/src/main.rs");
let mut attrs = vec![
KeyValue::new("path", full.clone()),
KeyValue::new("file_path", full.clone()),
KeyValue::new("cwd", full.clone()),
];
scrub_attributes(&mut attrs);
let blob = format!("{attrs:?}");
assert!(
!blob.contains(home_str.as_ref()),
"home path survived allowlisted scrub: {blob}"
);
assert!(
blob.contains("main.rs") || blob.contains("[HOME]") || blob.contains("~"),
"expected redacted path to retain a filename or home marker: {blob}"
);
}
#[test]
fn error_key_value_is_secret_and_path_scrubbed() {
// Free-form `error` strings are allowlisted for classification labels;
// any secret/path content that sneaks in must still be scrubbed.
let home = dirs::home_dir().expect("home dir");
let home_str = home.to_string_lossy();
let msg =
format!("failed reading {home_str}/.config/creds with sk-CANARYabcdefghij1234567890");
let mut attrs = vec![KeyValue::new("error", msg)];
scrub_attributes(&mut attrs);
let blob = format!("{attrs:?}");
assert!(!blob.contains("CANARY"), "secret survived in error: {blob}");
if home_str.len() >= 4 {
assert!(
!blob.contains(home_str.as_ref()),
"home path survived in error: {blob}"
);
}
}
}