grok-build-upstream-mirror/crates/codegen/xai-grok-workspace/src/discovery.rs

582 lines
22 KiB
Rust
Raw Normal View History

//! Skill, plugin, project-config, and permissions discovery.
//!
//! Phase 3a delegates to the existing discovery implementations in
//! `xai-grok-agent` and `xai-grok-tools` rather than duplicating them.
//! The workspace stores configuration on [`WorkspaceShared`] and the
//! [`MpscChannel`] methods call these helpers to return real data
//! instead of `Value::Null` stubs.
//!
//! Re-exports [`AgentsMdTracker`] from `xai-grok-tools` for external
//! consumers that need per-session project-instruction tracking.
use std::path::Path;
use serde_json::Value;
// Re-export AgentsMdTracker so consumers can reference it via the
// workspace crate without a direct xai-grok-tools dependency.
pub use xai_grok_tools::types::agents_md_tracker::AgentsMdTracker;
// Re-export the config types that callers pass into WorkspaceConfig.
pub use xai_grok_agent::plugins::discovery::DiscoveryConfig as PluginDiscoveryConfig;
pub use xai_grok_agent::plugins::trust::TrustStore as PluginTrustStore;
pub use xai_grok_agent::prompt::skills::SkillsConfig;
// ---------------------------------------------------------------------------
// Skill discovery
// ---------------------------------------------------------------------------
/// Discover skills visible from the workspace root.
///
/// Delegates to [`xai_grok_agent::prompt::skills::list_skills`] with
/// the workspace's `root_cwd` and the caller-supplied `SkillsConfig`.
/// Returns each [`SkillInfo`] serialized to a `serde_json::Value`.
///
/// The underlying `list_skills` implementation performs filesystem
/// I/O (stat + read for each SKILL.md) and does not hold any async
/// locks across `.await` points, so contention is not a concern.
pub async fn discover_skills(root_cwd: &Path, config: &SkillsConfig) -> Vec<Value> {
let cwd_str = root_cwd.to_string_lossy();
// Workspace discovery is out of scope for per-vendor compat gating;
// use the all-on default to preserve prior behavior.
let skills = xai_grok_agent::prompt::skills::list_skills(
Some(&cwd_str),
config,
xai_grok_agent::prompt::skills::CompatConfig::default(),
)
.await;
skills
.into_iter()
.filter_map(|s| match serde_json::to_value(&s) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
skill = %s.name,
error = %e,
"failed to serialize SkillInfo; dropping from response"
);
None
}
})
.collect()
}
// ---------------------------------------------------------------------------
// AGENTS.md discovery
// ---------------------------------------------------------------------------
/// Discover project-instruction files (AGENTS.md, Claude.md, rules) from the workspace root up to the git root.
pub async fn discover_agents_md(root_cwd: &Path) -> Vec<Value> {
let cwd_str = root_cwd.to_string_lossy();
let files = xai_grok_agent::prompt::agents_md::read_agents_config_with_paths(
&cwd_str,
xai_grok_tools::types::compat::CompatConfig::default(),
)
.await;
files
.into_iter()
.filter_map(|file| match serde_json::to_value(&file) {
Ok(v) => Some(v),
Err(e) => {
tracing::warn!(
file = %file.file_path,
error = %e,
"failed to serialize AgentConfigFile; dropping from response"
);
None
}
})
.collect()
}
// ---------------------------------------------------------------------------
// Plugin discovery
// ---------------------------------------------------------------------------
/// Discover plugins visible from the workspace root.
///
/// Delegates to [`xai_grok_agent::plugins::discover_plugins`] with
/// the workspace's `root_cwd` and the caller-supplied
/// [`PluginDiscoveryConfig`] and [`PluginTrustStore`].
///
/// Since [`DiscoveredPlugin`] does not derive `Serialize`, each
/// plugin is converted to a JSON object with the essential fields
/// that downstream consumers need (name, scope, root, trusted,
/// has_skills, has_hooks, has_mcp).
///
/// `project_trusted` is the folder-trust verdict for `root_cwd`, threaded into
/// discovery to gate Project-scope plugins.
pub fn discover_plugins(
root_cwd: &Path,
config: &PluginDiscoveryConfig,
trust_store: &PluginTrustStore,
project_trusted: bool,
) -> Vec<Value> {
let discovered = xai_grok_agent::plugins::discover_plugins(
Some(root_cwd),
config,
trust_store,
project_trusted,
);
discovered
.into_iter()
.map(|dp| {
serde_json::json!({
"name": dp.manifest.name,
"id": dp.id.0,
"root": dp.root.to_string_lossy(),
"scope": dp.scope.to_string(),
"trusted": dp.trusted,
"version": dp.manifest.version,
"description": dp.manifest.description,
"has_skills": !dp.skill_dirs.is_empty(),
"has_agents": !dp.agent_dirs.is_empty(),
"has_hooks": dp.hooks_path.is_some(),
"has_mcp": dp.mcp_config_path.is_some(),
"has_lsp": dp.lsp_config_path.is_some(),
"conflict": dp.conflict,
})
})
.collect()
}
// ---------------------------------------------------------------------------
// Project config
// ---------------------------------------------------------------------------
/// Load the project config from `<root_cwd>/.grok/config.toml`.
///
/// Returns `Value::Null` if the file does not exist or cannot be
/// parsed. Non-fatal errors are logged.
pub fn load_project_config(root_cwd: &Path) -> Value {
let config_path = root_cwd.join(".grok").join("config.toml");
match xai_grok_config::load_config_file(&config_path) {
Ok(toml::Value::Table(ref t)) if t.is_empty() => {
// The config loader returns an empty table when the file
// does not exist. Normalize to Null for callers.
Value::Null
}
Ok(toml_val) => toml_to_json(&toml_val),
Err(e) => {
tracing::warn!(
path = %config_path.display(),
error = %e,
"failed to load project config"
);
Value::Null
}
}
}
/// Convert a `toml::Value` to a `serde_json::Value`.
///
/// TOML's type system is close to JSON's. The main difference is
/// TOML's `Datetime` type which maps to a JSON string.
fn toml_to_json(v: &toml::Value) -> Value {
match v {
toml::Value::String(s) => Value::String(s.clone()),
toml::Value::Integer(i) => serde_json::json!(i),
toml::Value::Float(f) => serde_json::json!(f),
toml::Value::Boolean(b) => Value::Bool(*b),
toml::Value::Datetime(dt) => Value::String(dt.to_string()),
toml::Value::Array(arr) => Value::Array(arr.iter().map(toml_to_json).collect()),
toml::Value::Table(table) => {
let map: serde_json::Map<String, Value> = table
.iter()
.map(|(k, v)| (k.clone(), toml_to_json(v)))
.collect();
Value::Object(map)
}
}
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
/// Load the effective permission configuration for the workspace.
///
/// Delegates to
/// [`resolution::resolve_permissions_with_provenance`] which
/// merges rules from requirements.toml, managed-settings.json,
/// managed_config.toml, config.toml, and `.claude/settings.json`.
///
Synced from monorepo Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
2026-07-22 19:18:53 +01:00
/// `project_trusted` gates project-tier permission sources (same contract as
/// env/hooks/plugins). Hub/cloud callers outside the local folder-trust model
/// should pass `true`.
///
/// Returns a JSON object with `sources`, `loaded` (rule count), and
/// `skipped` (unrecognized rules). Returns `Value::Null` if no
/// permission sources are configured.
Synced from monorepo Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
2026-07-22 19:18:53 +01:00
pub async fn load_permissions(root_cwd: &Path, project_trusted: bool) -> Value {
use crate::permission::resolution;
Synced from monorepo Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
2026-07-22 19:18:53 +01:00
let Some(resolved) =
resolution::resolve_permissions_with_provenance(root_cwd, project_trusted).await
else {
return Value::Null;
};
let mut sources: Vec<String> = Vec::new();
for s in resolved.sources.iter().map(|s| s.to_string()) {
if !sources.contains(&s) {
sources.push(s);
}
}
let skipped: Vec<Value> = resolved
.skipped
.iter()
.map(|s| {
serde_json::json!({
"rule": s.rule,
"reason": s.reason,
})
})
.collect();
serde_json::json!({
"sources": sources,
"loaded": resolved.config.rules.len(),
"skipped": skipped,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
// ---- Skill discovery tests ----
// Note: `list_skills` also discovers user-scoped skills from
// `~/.grok/skills/`, so on a developer machine the result may be
// non-empty even for an empty workspace. Tests below check for
// specific skills rather than asserting emptiness.
#[tokio::test]
async fn discover_skills_finds_skill_md() {
let tmp = tempfile::tempdir().unwrap();
let skills_dir = tmp.path().join(".grok").join("skills").join("my-skill");
fs::create_dir_all(&skills_dir).unwrap();
fs::write(
skills_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: A test skill\n---\n# Body\n",
)
.unwrap();
let skills = discover_skills(tmp.path(), &SkillsConfig::default()).await;
let found = skills
.iter()
.find(|s| s["name"].as_str() == Some("my-skill"));
assert!(found.is_some(), "should find my-skill");
let skill = found.unwrap();
assert_eq!(skill["description"].as_str(), Some("A test skill"));
}
#[tokio::test]
async fn discover_skills_respects_ignore_config() {
let tmp = tempfile::tempdir().unwrap();
let skills_dir = tmp.path().join(".grok").join("skills").join("ignored");
fs::create_dir_all(&skills_dir).unwrap();
fs::write(
skills_dir.join("SKILL.md"),
"---\nname: ignored\ndescription: Ignored\n---\n",
)
.unwrap();
let config = SkillsConfig {
paths: vec![],
ignore: vec![tmp.path().to_string_lossy().to_string()],
disabled: vec![],
server_skill_dirs: vec![],
bundled_skill_dirs: vec![],
};
let skills = discover_skills(tmp.path(), &config).await;
let found = skills.iter().any(|s| s["name"].as_str() == Some("ignored"));
assert!(
!found,
"skill in ignore path should be filtered out of results"
);
}
#[tokio::test]
async fn discover_skills_returns_serialized_values() {
let tmp = tempfile::tempdir().unwrap();
let skills_dir = tmp
.path()
.join(".grok")
.join("skills")
.join("serialized-check");
fs::create_dir_all(&skills_dir).unwrap();
fs::write(
skills_dir.join("SKILL.md"),
"---\nname: serialized-check\ndescription: Test serialization\n---\n",
)
.unwrap();
let skills = discover_skills(tmp.path(), &SkillsConfig::default()).await;
let found = skills
.iter()
.find(|s| s["name"].as_str() == Some("serialized-check"))
.expect("should find serialized-check");
// SkillInfo has these required fields when serialized
assert!(found["path"].is_string(), "path should be a string");
assert!(found["scope"].is_string(), "scope should be serialized");
}
// ---- AGENTS.md discovery tests ----
#[test]
fn agent_config_file_wire_matches_workspace_types_mirror() {
// The RPC serializes grok-build's AgentConfigFile and the remote
// consumer deserializes the workspace-types mirror; pin the cross-crate
// serde shape so a rename/attr drift on either side can't silently
// break discovery.
let src = xai_grok_agent::prompt::agents_md::AgentConfigFile {
file_name: "AGENTS.md".to_string(),
file_path: "/repo/AGENTS.md".to_string(),
content: "# Instructions\n".to_string(),
};
let json = serde_json::to_value(&src).unwrap();
let mirror: xai_grok_workspace_types::rpc::agents_md::AgentConfigFile =
serde_json::from_value(json.clone())
.expect("server shape must deserialize into mirror");
assert_eq!(mirror.file_name, src.file_name);
assert_eq!(mirror.file_path, src.file_path);
assert_eq!(mirror.content, src.content);
assert_eq!(
serde_json::to_value(&mirror).unwrap(),
json,
"mirror must serialize to the exact shape the server emits"
);
}
// Discovery also scans the real `~/.grok`, so fixtures use test-unique names.
#[tokio::test]
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
2026-07-18 19:48:28 +01:00
async fn discover_agents_md_receives_normalized_rule_content() {
let tmp = tempfile::tempdir().unwrap();
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
2026-07-18 19:48:28 +01:00
let rules_dir = tmp.path().join(".cursor").join("rules");
fs::create_dir_all(&rules_dir).unwrap();
fs::write(
rules_dir.join("xyzzy-discover-agents-md-test.md"),
"---\nglobs: [\"*.rs\"]\n---\nUse tabs, not spaces.\n",
)
.unwrap();
let files = discover_agents_md(tmp.path()).await;
let rule = files
.iter()
.find(|f| {
f["file_path"]
.as_str()
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
2026-07-18 19:48:28 +01:00
.is_some_and(|p| p.ends_with("/.cursor/rules/xyzzy-discover-agents-md-test.md"))
})
.expect("should discover the rules file");
let content = rule["content"].as_str().unwrap();
assert!(
content.contains("Use tabs, not spaces."),
"rule body must survive: {content}"
);
assert!(
!content.contains("globs:"),
"YAML frontmatter must be stripped: {content}"
);
}
#[tokio::test]
async fn discover_agents_md_keeps_non_rules_frontmatter() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("AGENTS.md"),
"---\nXYZZY_LEADING_DASHES_MARKER\n---\nBody after horizontal rules.\n",
)
.unwrap();
let files = discover_agents_md(tmp.path()).await;
let agents = files
.iter()
.find(|f| {
f["content"]
.as_str()
.is_some_and(|c| c.contains("XYZZY_LEADING_DASHES_MARKER"))
})
.expect("should discover the AGENTS.md with its leading dashes intact");
let content = agents["content"].as_str().unwrap();
assert!(
content.starts_with("---\n"),
"stripping must be rules-files-only: {content}"
);
}
// ---- Plugin discovery tests ----
// Note: `discover_plugins` also discovers user-scoped plugins
// from `~/.grok/plugins/`, so tests check for specific plugins.
#[test]
fn discover_plugins_finds_manifest_plugin() {
let tmp = tempfile::tempdir().unwrap();
let plugins_dir = tmp.path().join(".grok").join("plugins").join("test-plugin");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(
plugins_dir.join("plugin.json"),
r#"{"name": "test-plugin"}"#,
)
.unwrap();
// Plugin needs at least one component to be recognized
fs::create_dir_all(plugins_dir.join("skills")).unwrap();
let trust = PluginTrustStore::load_from(tmp.path().join("trust"));
let config = PluginDiscoveryConfig::default();
let plugins = discover_plugins(tmp.path(), &config, &trust, true);
let found = plugins
.iter()
.find(|p| p["name"].as_str() == Some("test-plugin"));
assert!(found.is_some(), "should find test-plugin");
let p = found.unwrap();
assert_eq!(p["scope"].as_str(), Some("project"));
assert_eq!(p["has_skills"].as_bool(), Some(true));
}
#[test]
fn discover_plugins_json_has_expected_fields() {
let tmp = tempfile::tempdir().unwrap();
let plugins_dir = tmp.path().join(".grok").join("plugins").join("field-test");
fs::create_dir_all(plugins_dir.join("skills")).unwrap();
fs::write(
plugins_dir.join("plugin.json"),
r#"{"name": "field-test", "version": "1.0.0", "description": "Test plugin"}"#,
)
.unwrap();
let trust = PluginTrustStore::load_from(tmp.path().join("trust"));
let config = PluginDiscoveryConfig::default();
let plugins = discover_plugins(tmp.path(), &config, &trust, true);
let p = plugins
.iter()
.find(|p| p["name"].as_str() == Some("field-test"))
.expect("should find field-test");
assert_eq!(p["version"].as_str(), Some("1.0.0"));
assert_eq!(p["description"].as_str(), Some("Test plugin"));
assert!(p["id"].is_string());
assert!(p["root"].is_string());
assert!(p["trusted"].is_boolean());
assert!(p["has_hooks"].is_boolean());
assert!(p["has_mcp"].is_boolean());
assert!(p["has_lsp"].is_boolean());
assert!(p["has_agents"].is_boolean());
assert!(p["has_skills"].is_boolean());
}
// ---- Project config tests ----
#[test]
fn load_project_config_missing_file_returns_null() {
let tmp = tempfile::tempdir().unwrap();
let config = load_project_config(tmp.path());
assert_eq!(config, Value::Null, "missing config → Null");
}
#[test]
fn load_project_config_reads_toml_as_json() {
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".grok");
fs::create_dir_all(&grok_dir).unwrap();
fs::write(
grok_dir.join("config.toml"),
"[skills]\npaths = [\"/extra/skills\"]\n\n[plugins]\ndisabled = [\"noisy-plugin\"]\n",
)
.unwrap();
let config = load_project_config(tmp.path());
assert!(config.is_object(), "parsed config should be an object");
assert!(
config["skills"]["paths"].is_array(),
"skills.paths should be an array"
);
assert_eq!(config["skills"]["paths"][0].as_str(), Some("/extra/skills"));
assert_eq!(
config["plugins"]["disabled"][0].as_str(),
Some("noisy-plugin")
);
}
// ---- toml_to_json tests ----
#[test]
fn toml_to_json_basic_types() {
let toml_val: toml::Value =
toml::from_str("s = \"hello\"\ni = 42\nf = 3.14\nb = true\n").unwrap();
let json = toml_to_json(&toml_val);
assert_eq!(json["s"], "hello");
assert_eq!(json["i"], 42);
assert_eq!(json["b"], true);
}
#[test]
fn toml_to_json_nested_table() {
let toml_val: toml::Value = toml::from_str("[section]\nkey = \"value\"\n").unwrap();
let json = toml_to_json(&toml_val);
assert_eq!(json["section"]["key"], "value");
}
#[test]
fn toml_to_json_array() {
let toml_val: toml::Value = toml::from_str("items = [1, 2, 3]\n").unwrap();
let json = toml_to_json(&toml_val);
assert_eq!(json["items"][0], 1);
assert_eq!(json["items"][2], 3);
}
// ---- Permissions tests ----
// Note: `resolve_permissions_with_provenance` checks system-managed
// settings and requirements.toml from the global config, so on a
// developer machine with Grok installed it may return non-Null even
// for a temp directory. Both branches assert a concrete condition.
#[tokio::test]
async fn load_permissions_returns_valid_json() {
let tmp = tempfile::tempdir().unwrap();
Synced from monorepo Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
2026-07-22 19:18:53 +01:00
let result = load_permissions(tmp.path(), true).await;
// Result is either Null (no sources) or an object with
// sources, loaded, and skipped fields. Both branches assert
// a definite pass criterion.
if result.is_null() {
// No permission sources on this machine — Null is correct.
assert_eq!(result, Value::Null, "expected Null for empty workspace");
} else {
assert!(result["sources"].is_array(), "sources should be an array");
assert!(result["loaded"].is_number(), "loaded should be a number");
assert!(result["skipped"].is_array(), "skipped should be an array");
}
}
#[tokio::test]
async fn load_permissions_with_settings_file_returns_object() {
let tmp = tempfile::tempdir().unwrap();
// Create a minimal .claude/settings.json with a permission rule
// so the test always exercises the non-null path.
let claude_dir = tmp.path().join(".claude");
fs::create_dir_all(&claude_dir).unwrap();
fs::write(
claude_dir.join("settings.json"),
r#"{"permissions":{"allow":["Bash(git status)"]}}"#,
)
.unwrap();
Synced from monorepo Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
2026-07-22 19:18:53 +01:00
let result = load_permissions(tmp.path(), true).await;
assert!(result.is_object(), "should return an object, got {result}");
assert!(result["sources"].is_array(), "sources should be an array");
assert!(result["loaded"].is_number(), "loaded should be a number");
assert!(result["skipped"].is_array(), "skipped should be an array");
assert!(
result["loaded"].as_u64().unwrap_or(0) >= 1,
"should have at least one loaded rule"
);
}
}