Synced from monorepo
Changes: - grok-shell: request workspaces:read/write OAuth2 scopes - security: fix SSRF bypass via HTTP redirect in hook runner - fix(grok-build): enterprise STT WSS URL + API-key voice bearer - Harden identity-change purge and sync-marker invariants - sandbox + workspace-server: delete the legacy ready-file arm - Show billing URL when browser cannot open - fix(pager): show folder-trust UI in minimal mode - fix(pager): drain task_backgrounded before no-wait headless exit - grok-agent-sdk: stop SDK-spawned agents from staging self-updates they can never adopt - Split settings_modal into directory module - Delegate VS Code SSH file links - grok-shell: release the workspace session binding when a session is removed - keep skills reachable when their name collides with a client builtin - Preserve semantic link targets
This commit is contained in:
parent
c68e39f604
commit
8adf9013a0
117 changed files with 16998 additions and 14540 deletions
|
|
@ -10,9 +10,23 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use xai_grok_tools::implementations::skills::types::SkillScope;
|
||||
|
||||
use super::acp_command::AcpSlashCommand;
|
||||
use super::command::SlashCommand;
|
||||
|
||||
fn client_collision_qualified_name(
|
||||
cmd: &agent_client_protocol::AvailableCommand,
|
||||
) -> Option<String> {
|
||||
let meta = cmd.meta.as_ref()?;
|
||||
meta.get("path").and_then(|v| v.as_str())?;
|
||||
let scope: SkillScope = serde_json::from_value(meta.get("scope")?.clone()).ok()?;
|
||||
if scope == SkillScope::Plugin {
|
||||
return None;
|
||||
}
|
||||
Some(format!("{}:{}", scope.as_ref(), cmd.name))
|
||||
}
|
||||
|
||||
/// Source of a command in the registry. Used for precedence and replacement.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CommandSource {
|
||||
|
|
@ -487,16 +501,20 @@ impl CommandRegistry {
|
|||
"reload-plugins",
|
||||
];
|
||||
|
||||
// Add new ACP commands, skipping collisions with builtins and blocked names.
|
||||
for acp_cmd in commands {
|
||||
let name_lower = acp_cmd.name.to_lowercase();
|
||||
if builtin_keys.contains(&name_lower) {
|
||||
continue;
|
||||
}
|
||||
if BLOCKED_NAMES
|
||||
.iter()
|
||||
.any(|b| b.eq_ignore_ascii_case(&name_lower))
|
||||
{
|
||||
let name_reserved = builtin_keys.contains(&name_lower)
|
||||
|| BLOCKED_NAMES
|
||||
.iter()
|
||||
.any(|b| b.eq_ignore_ascii_case(&name_lower));
|
||||
if name_reserved {
|
||||
if let Some(qualified) = client_collision_qualified_name(acp_cmd) {
|
||||
let mut renamed = acp_cmd.clone();
|
||||
renamed.name = qualified;
|
||||
self.commands
|
||||
.push(Arc::new(AcpSlashCommand::from(&renamed)));
|
||||
self.sources.push(CommandSource::Acp);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
self.commands.push(Arc::new(AcpSlashCommand::from(acp_cmd)));
|
||||
|
|
@ -945,6 +963,108 @@ mod tests {
|
|||
assert_eq!(registry.command_count(), 1);
|
||||
}
|
||||
|
||||
fn acp_skill(name: &str, scope: &str) -> agent_client_protocol::AvailableCommand {
|
||||
let meta = serde_json::json!({ "scope": scope, "path": "/x/SKILL.md" })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
agent_client_protocol::AvailableCommand::new(name.to_string(), format!("{name} skill"))
|
||||
.meta(meta)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_nonplugin_skill_colliding_with_builtin_is_requalified() {
|
||||
let builtin: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
name: "login",
|
||||
aliases: &[],
|
||||
});
|
||||
let mut registry = CommandRegistry::new(vec![builtin]);
|
||||
registry.set_acp_commands(&[acp_skill("login", "local")]);
|
||||
|
||||
assert!(registry.get("login").is_some());
|
||||
assert!(registry.is_builtin("login"));
|
||||
assert!(registry.get("local:login").is_some());
|
||||
assert!(!registry.is_builtin("local:login"));
|
||||
assert_eq!(registry.command_count(), 2, "builtin + re-homed skill");
|
||||
assert!(
|
||||
registry
|
||||
.triggers()
|
||||
.iter()
|
||||
.any(|t| t.canonical == "local:login"),
|
||||
"re-homed skill should have a dropdown trigger"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_malformed_skill_meta_colliding_with_builtin_is_dropped() {
|
||||
let builtin: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
name: "login",
|
||||
aliases: &[],
|
||||
});
|
||||
let mut registry = CommandRegistry::new(vec![builtin]);
|
||||
let meta = serde_json::json!({ "scope": "local" })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let cmd = agent_client_protocol::AvailableCommand::new(
|
||||
"login".to_string(),
|
||||
"malformed".to_string(),
|
||||
)
|
||||
.meta(meta);
|
||||
registry.set_acp_commands(&[cmd]);
|
||||
assert_eq!(
|
||||
registry.command_count(),
|
||||
1,
|
||||
"malformed-meta collision drops"
|
||||
);
|
||||
assert!(registry.get("local:login").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_skill_named_after_blocked_name_is_requalified() {
|
||||
let builtin: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
name: "exit",
|
||||
aliases: &[],
|
||||
});
|
||||
let mut registry = CommandRegistry::new(vec![builtin]);
|
||||
registry.set_acp_commands(&[acp_skill("hooks-add", "local")]);
|
||||
assert!(registry.get("local:hooks-add").is_some());
|
||||
assert!(registry.get("hooks-add").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_plugin_skill_colliding_with_builtin_is_dropped_not_requalified() {
|
||||
let builtin: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
name: "login",
|
||||
aliases: &[],
|
||||
});
|
||||
let mut registry = CommandRegistry::new(vec![builtin]);
|
||||
registry.set_acp_commands(&[acp_skill("login", "plugin")]);
|
||||
|
||||
assert!(registry.get("login").is_some());
|
||||
assert!(registry.is_builtin("login"));
|
||||
assert!(
|
||||
registry.get("plugin:login").is_none(),
|
||||
"pager must not fabricate a plugin-qualified name"
|
||||
);
|
||||
assert_eq!(registry.command_count(), 1, "only the builtin remains");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acp_nonskill_colliding_with_builtin_is_dropped() {
|
||||
let builtin: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
name: "login",
|
||||
aliases: &[],
|
||||
});
|
||||
let mut registry = CommandRegistry::new(vec![builtin]);
|
||||
registry.set_acp_commands(&[agent_client_protocol::AvailableCommand::new(
|
||||
"login".to_string(),
|
||||
"shell login".to_string(),
|
||||
)]);
|
||||
assert_eq!(registry.command_count(), 1);
|
||||
assert!(registry.is_builtin("login"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_without_required_tools_is_always_visible() {
|
||||
let plain: Arc<dyn SlashCommand> = Arc::new(DummyCommand {
|
||||
|
|
|
|||
Loading…
Reference in a new issue