Skip to main content

agent_config/agents/
antigravity.rs

1//! Google Antigravity integration.
2//!
3//! Two surfaces:
4//!
5//! 1. **Rules** — project-local markdown files at `.agents/rules/<tag>.md`.
6//!    Legacy `.agent/rules/<tag>.md` installs are still detected and removed.
7//!
8//! 2. **Skills** — directory-scoped skills at `.agents/skills/<name>/` (Local)
9//!    or `~/.gemini/antigravity/skills/<name>/` (Global). Each skill is a
10//!    folder with `SKILL.md` plus optional `scripts/`/`references/`/`assets/`.
11//!    Legacy `.agent/skills/<name>/` installs are still detected and removed.
12//!
13//! 3. **MCP servers** — JSON config at `.agent/mcp_config.json` (Local) or
14//!    `~/.gemini/antigravity/mcp_config.json` (Global), keyed by server name
15//!    under `mcpServers`.
16//!
17//! Antigravity does not yet expose a hooks surface in the way other harnesses
18//! do.
19
20use std::path::PathBuf;
21
22use crate::agents::planning as agent_planning;
23use crate::error::AgentConfigError;
24use crate::integration::{
25    InstallReport, InstructionSurface, Integration, McpSurface, SkillSurface, UninstallReport,
26};
27use crate::paths;
28use crate::plan::{InstallPlan, UninstallPlan};
29use crate::scope::{Scope, ScopeKind};
30use crate::spec::{HookSpec, InstructionSpec, McpSpec, SkillSpec};
31use crate::status::StatusReport;
32use crate::util::{instructions_dir, mcp_json_object, ownership, rules_dir, skills_dir};
33
34const RULES_DIR: &str = ".agents/rules";
35const LEGACY_RULES_DIR: &str = ".agent/rules";
36
37/// Google Antigravity integration.
38#[derive(Debug, Clone, Copy, Default)]
39pub struct AntigravityAgent {
40    _private: (),
41}
42
43impl AntigravityAgent {
44    /// Construct an instance. Stateless.
45    pub const fn new() -> Self {
46        Self { _private: () }
47    }
48
49    fn project_root<'a>(&self, scope: &'a Scope) -> Result<&'a std::path::Path, AgentConfigError> {
50        match scope {
51            Scope::Local(p) => Ok(p),
52            Scope::Global => Err(AgentConfigError::UnsupportedScope {
53                id: "antigravity",
54                scope: ScopeKind::Global,
55            }),
56        }
57    }
58
59    /// Skills root: `<root>/.agents/skills/` (Local) or
60    /// `~/.gemini/antigravity/skills/` (Global). Both scopes are supported
61    /// for skills.
62    fn skills_root(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
63        Ok(match scope {
64            Scope::Global => paths::gemini_home()?.join("antigravity").join("skills"),
65            Scope::Local(p) => p.join(".agents").join("skills"),
66        })
67    }
68
69    fn legacy_skills_root(scope: &Scope) -> Option<PathBuf> {
70        match scope {
71            Scope::Global => None,
72            Scope::Local(p) => Some(p.join(".agent").join("skills")),
73        }
74    }
75
76    fn existing_skills_root(scope: &Scope, name: &str) -> Result<PathBuf, AgentConfigError> {
77        SkillSpec::validate_name(name)?;
78        let root = Self::skills_root(scope)?;
79        let (dir, _, ledger) = skills_dir::paths_for_status(&root, name);
80        if dir.exists() || ownership::owner_of(&ledger, name)?.is_some() {
81            return Ok(root);
82        }
83
84        if let Some(legacy) = Self::legacy_skills_root(scope) {
85            let (dir, _, ledger) = skills_dir::paths_for_status(&legacy, name);
86            if dir.exists() || ownership::owner_of(&ledger, name)?.is_some() {
87                return Ok(legacy);
88            }
89        }
90
91        Ok(root)
92    }
93
94    fn mcp_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
95        Ok(match scope {
96            Scope::Global => paths::antigravity_mcp_global_file()?,
97            Scope::Local(p) => p.join(".agent").join("mcp_config.json"),
98        })
99    }
100}
101
102impl Integration for AntigravityAgent {
103    fn id(&self) -> &'static str {
104        "antigravity"
105    }
106
107    fn display_name(&self) -> &'static str {
108        "Google Antigravity"
109    }
110
111    fn supported_scopes(&self) -> &'static [ScopeKind] {
112        &[ScopeKind::Local]
113    }
114
115    fn status(&self, scope: &Scope, tag: &str) -> Result<StatusReport, AgentConfigError> {
116        HookSpec::validate_tag(tag)?;
117        let root = self.project_root(scope)?;
118        let path = rules_dir::target_path(root, RULES_DIR, tag);
119        if !path.exists() {
120            let legacy = rules_dir::target_path(root, LEGACY_RULES_DIR, tag);
121            if legacy.exists() {
122                return Ok(StatusReport::for_file_hook(tag, legacy));
123            }
124        }
125        Ok(StatusReport::for_file_hook(tag, path))
126    }
127
128    fn plan_install(
129        &self,
130        scope: &Scope,
131        spec: &HookSpec,
132    ) -> Result<InstallPlan, AgentConfigError> {
133        agent_planning::rules_install(
134            Integration::id(self),
135            scope,
136            spec,
137            self.project_root(scope),
138            RULES_DIR,
139        )
140    }
141
142    fn plan_uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallPlan, AgentConfigError> {
143        HookSpec::validate_tag(tag)?;
144        let root = self.project_root(scope);
145        let rules_dir = if let Ok(root) = root {
146            let current = rules_dir::target_path(root, RULES_DIR, tag);
147            let legacy = rules_dir::target_path(root, LEGACY_RULES_DIR, tag);
148            if !current.exists() && legacy.exists() {
149                LEGACY_RULES_DIR
150            } else {
151                RULES_DIR
152            }
153        } else {
154            RULES_DIR
155        };
156        agent_planning::rules_uninstall(
157            Integration::id(self),
158            scope,
159            tag,
160            self.project_root(scope),
161            rules_dir,
162        )
163    }
164
165    fn install(&self, scope: &Scope, spec: &HookSpec) -> Result<InstallReport, AgentConfigError> {
166        HookSpec::validate_tag(&spec.tag)?;
167        let _ = self.project_root(scope)?;
168        let rules = spec
169            .rules
170            .as_ref()
171            .ok_or(AgentConfigError::MissingSpecField {
172                id: "antigravity",
173                field: "rules",
174            })?;
175        rules_dir::install(scope, RULES_DIR, &spec.tag, &rules.content)
176    }
177
178    fn uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallReport, AgentConfigError> {
179        HookSpec::validate_tag(tag)?;
180        let root = self.project_root(scope)?;
181        let current = rules_dir::target_path(root, RULES_DIR, tag);
182        let legacy = rules_dir::target_path(root, LEGACY_RULES_DIR, tag);
183        let rules_dir = if !current.exists() && legacy.exists() {
184            LEGACY_RULES_DIR
185        } else {
186            RULES_DIR
187        };
188        rules_dir::uninstall(scope, rules_dir, tag)
189    }
190}
191
192impl McpSurface for AntigravityAgent {
193    fn id(&self) -> &'static str {
194        "antigravity"
195    }
196
197    fn supported_mcp_scopes(&self) -> &'static [ScopeKind] {
198        &[ScopeKind::Global, ScopeKind::Local]
199    }
200
201    fn mcp_status(
202        &self,
203        scope: &Scope,
204        name: &str,
205        expected_owner: &str,
206    ) -> Result<StatusReport, AgentConfigError> {
207        McpSpec::validate_name(name)?;
208        let cfg = Self::mcp_path(scope)?;
209        let ledger = ownership::mcp_ledger_for(&cfg);
210        let presence = mcp_json_object::config_presence(&cfg, name)?;
211        let recorded = ownership::owner_of(&ledger, name)?;
212        Ok(StatusReport::for_mcp(
213            name,
214            cfg,
215            ledger,
216            presence,
217            expected_owner,
218            recorded,
219        ))
220    }
221
222    fn plan_install_mcp(
223        &self,
224        scope: &Scope,
225        spec: &McpSpec,
226    ) -> Result<InstallPlan, AgentConfigError> {
227        agent_planning::mcp_json_object_install(
228            McpSurface::id(self),
229            scope,
230            spec,
231            Self::mcp_path(scope),
232        )
233    }
234
235    fn plan_uninstall_mcp(
236        &self,
237        scope: &Scope,
238        name: &str,
239        owner_tag: &str,
240    ) -> Result<UninstallPlan, AgentConfigError> {
241        agent_planning::mcp_json_object_uninstall(
242            McpSurface::id(self),
243            scope,
244            name,
245            owner_tag,
246            Self::mcp_path(scope),
247        )
248    }
249
250    fn install_mcp(
251        &self,
252        scope: &Scope,
253        spec: &McpSpec,
254    ) -> Result<InstallReport, AgentConfigError> {
255        spec.validate()?;
256        let cfg = Self::mcp_path(scope)?;
257        spec.validate_local_secret_policy(scope)?;
258        scope.ensure_contained(&cfg)?;
259        let ledger = ownership::mcp_ledger_for(&cfg);
260        mcp_json_object::install(&cfg, &ledger, spec)
261    }
262
263    fn uninstall_mcp(
264        &self,
265        scope: &Scope,
266        name: &str,
267        owner_tag: &str,
268    ) -> Result<UninstallReport, AgentConfigError> {
269        McpSpec::validate_name(name)?;
270        HookSpec::validate_tag(owner_tag)?;
271        let cfg = Self::mcp_path(scope)?;
272        scope.ensure_contained(&cfg)?;
273        let ledger = ownership::mcp_ledger_for(&cfg);
274        mcp_json_object::uninstall(&cfg, &ledger, name, owner_tag, "mcp server")
275    }
276}
277
278impl SkillSurface for AntigravityAgent {
279    fn id(&self) -> &'static str {
280        "antigravity"
281    }
282
283    fn supported_skill_scopes(&self) -> &'static [ScopeKind] {
284        &[ScopeKind::Global, ScopeKind::Local]
285    }
286
287    fn skill_status(
288        &self,
289        scope: &Scope,
290        name: &str,
291        expected_owner: &str,
292    ) -> Result<StatusReport, AgentConfigError> {
293        SkillSpec::validate_name(name)?;
294        let root = Self::skills_root(scope)?;
295        let (dir, manifest, ledger) = skills_dir::paths_for_status(&root, name);
296        if !dir.exists() && ownership::owner_of(&ledger, name)?.is_none() {
297            if let Some(legacy) = Self::legacy_skills_root(scope) {
298                let (legacy_dir, legacy_manifest, legacy_ledger) =
299                    skills_dir::paths_for_status(&legacy, name);
300                if legacy_dir.exists() || ownership::owner_of(&legacy_ledger, name)?.is_some() {
301                    let recorded = ownership::owner_of(&legacy_ledger, name)?;
302                    return Ok(StatusReport::for_skill(
303                        name,
304                        legacy_dir,
305                        legacy_manifest,
306                        legacy_ledger,
307                        expected_owner,
308                        recorded,
309                    ));
310                }
311            }
312        }
313        let recorded = ownership::owner_of(&ledger, name)?;
314        Ok(StatusReport::for_skill(
315            name,
316            dir,
317            manifest,
318            ledger,
319            expected_owner,
320            recorded,
321        ))
322    }
323
324    fn plan_install_skill(
325        &self,
326        scope: &Scope,
327        spec: &SkillSpec,
328    ) -> Result<InstallPlan, AgentConfigError> {
329        agent_planning::skill_install(
330            SkillSurface::id(self),
331            scope,
332            spec,
333            Self::skills_root(scope),
334        )
335    }
336
337    fn plan_uninstall_skill(
338        &self,
339        scope: &Scope,
340        name: &str,
341        owner_tag: &str,
342    ) -> Result<UninstallPlan, AgentConfigError> {
343        agent_planning::skill_uninstall(
344            SkillSurface::id(self),
345            scope,
346            name,
347            owner_tag,
348            Self::existing_skills_root(scope, name),
349        )
350    }
351
352    fn install_skill(
353        &self,
354        scope: &Scope,
355        spec: &SkillSpec,
356    ) -> Result<InstallReport, AgentConfigError> {
357        spec.validate()?;
358        let root = Self::skills_root(scope)?;
359        scope.ensure_contained(&root)?;
360        skills_dir::install(&root, spec)
361    }
362
363    fn uninstall_skill(
364        &self,
365        scope: &Scope,
366        name: &str,
367        owner_tag: &str,
368    ) -> Result<UninstallReport, AgentConfigError> {
369        SkillSpec::validate_name(name)?;
370        HookSpec::validate_tag(owner_tag)?;
371        let root = Self::existing_skills_root(scope, name)?;
372        scope.ensure_contained(&root)?;
373        skills_dir::uninstall(&root, name, owner_tag)
374    }
375}
376
377impl AntigravityAgent {
378    fn standalone_layout(
379        &self,
380        scope: &Scope,
381    ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
382        let root = self.project_root(scope)?;
383        Ok(instructions_dir::StandaloneLayout {
384            config_dir: root.join(".agents"),
385            instruction_dir: root.join(RULES_DIR),
386        })
387    }
388
389    fn legacy_standalone_layout(
390        &self,
391        scope: &Scope,
392    ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
393        let root = self.project_root(scope)?;
394        Ok(instructions_dir::StandaloneLayout {
395            config_dir: root.join(".agent"),
396            instruction_dir: root.join(LEGACY_RULES_DIR),
397        })
398    }
399
400    fn existing_standalone_layout(
401        &self,
402        scope: &Scope,
403        name: &str,
404    ) -> Result<instructions_dir::StandaloneLayout, AgentConfigError> {
405        InstructionSpec::validate_name(name)?;
406        let primary = self.standalone_layout(scope)?;
407        let primary_file = primary.instruction_dir.join(format!("{name}.md"));
408        let primary_ledger = instructions_dir::ledger_path(&primary.config_dir);
409        if primary_file.exists() || ownership::owner_of(&primary_ledger, name)?.is_some() {
410            return Ok(primary);
411        }
412
413        let legacy = self.legacy_standalone_layout(scope)?;
414        let legacy_file = legacy.instruction_dir.join(format!("{name}.md"));
415        let legacy_ledger = instructions_dir::ledger_path(&legacy.config_dir);
416        if legacy_file.exists() || ownership::owner_of(&legacy_ledger, name)?.is_some() {
417            return Ok(legacy);
418        }
419
420        Ok(primary)
421    }
422}
423
424impl InstructionSurface for AntigravityAgent {
425    fn id(&self) -> &'static str {
426        "antigravity"
427    }
428
429    fn supported_instruction_scopes(&self) -> &'static [ScopeKind] {
430        &[ScopeKind::Local]
431    }
432
433    fn instruction_status(
434        &self,
435        scope: &Scope,
436        name: &str,
437        expected_owner: &str,
438    ) -> Result<StatusReport, AgentConfigError> {
439        instructions_dir::standalone_status(
440            self.existing_standalone_layout(scope, name)?,
441            name,
442            expected_owner,
443        )
444    }
445
446    fn plan_install_instruction(
447        &self,
448        scope: &Scope,
449        spec: &InstructionSpec,
450    ) -> Result<InstallPlan, AgentConfigError> {
451        instructions_dir::standalone_plan_install(
452            InstructionSurface::id(self),
453            scope,
454            self.standalone_layout(scope),
455            spec,
456        )
457    }
458
459    fn plan_uninstall_instruction(
460        &self,
461        scope: &Scope,
462        name: &str,
463        owner_tag: &str,
464    ) -> Result<UninstallPlan, AgentConfigError> {
465        instructions_dir::standalone_plan_uninstall(
466            InstructionSurface::id(self),
467            scope,
468            self.existing_standalone_layout(scope, name),
469            name,
470            owner_tag,
471        )
472    }
473
474    fn install_instruction(
475        &self,
476        scope: &Scope,
477        spec: &InstructionSpec,
478    ) -> Result<InstallReport, AgentConfigError> {
479        instructions_dir::standalone_install(scope, self.standalone_layout(scope)?, spec)
480    }
481
482    fn uninstall_instruction(
483        &self,
484        scope: &Scope,
485        name: &str,
486        owner_tag: &str,
487    ) -> Result<UninstallReport, AgentConfigError> {
488        instructions_dir::standalone_uninstall(
489            scope,
490            self.existing_standalone_layout(scope, name)?,
491            name,
492            owner_tag,
493        )
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::spec::InstructionPlacement;
501    use std::fs;
502    use tempfile::tempdir;
503
504    fn rules_spec(tag: &str, body: &str) -> HookSpec {
505        HookSpec::builder(tag)
506            .command_program("noop", [] as [&str; 0])
507            .rules(body)
508            .build()
509    }
510
511    fn skill(name: &str, owner: &str) -> SkillSpec {
512        SkillSpec::builder(name)
513            .owner(owner)
514            .description("Format Git commits.")
515            .body("## Goal\nFormat them.\n")
516            .build()
517    }
518
519    fn mcp_spec(name: &str, owner: &str) -> McpSpec {
520        McpSpec::builder(name)
521            .owner(owner)
522            .stdio("npx", ["-y", "@example/server"])
523            .build()
524    }
525
526    fn instruction(name: &str, owner: &str) -> InstructionSpec {
527        InstructionSpec::builder(name)
528            .owner(owner)
529            .placement(InstructionPlacement::StandaloneFile)
530            .body("Use Antigravity instructions.\n")
531            .build()
532    }
533
534    #[test]
535    fn install_rules_uses_plural_dot_agents() {
536        let dir = tempdir().unwrap();
537        let agent = AntigravityAgent::new();
538        let scope = Scope::Local(dir.path().to_path_buf());
539        agent.install(&scope, &rules_spec("alpha", "body")).unwrap();
540        assert!(dir.path().join(".agents/rules/alpha.md").exists());
541        assert!(!dir.path().join(".agent/rules/alpha.md").exists());
542    }
543
544    #[test]
545    fn legacy_dot_agent_rules_status_and_uninstall_still_work() {
546        let dir = tempdir().unwrap();
547        let agent = AntigravityAgent::new();
548        let scope = Scope::Local(dir.path().to_path_buf());
549        fs::create_dir_all(dir.path().join(".agent/rules")).unwrap();
550        fs::write(dir.path().join(".agent/rules/alpha.md"), "legacy\n").unwrap();
551
552        assert!(agent.is_installed(&scope, "alpha").unwrap());
553        agent.uninstall(&scope, "alpha").unwrap();
554        assert!(!dir.path().join(".agent/rules/alpha.md").exists());
555    }
556
557    #[test]
558    fn rules_install_idempotent() {
559        let dir = tempdir().unwrap();
560        let agent = AntigravityAgent::new();
561        let scope = Scope::Local(dir.path().to_path_buf());
562        let s = rules_spec("alpha", "body");
563        agent.install(&scope, &s).unwrap();
564        let r = agent.install(&scope, &s).unwrap();
565        assert!(r.already_installed);
566    }
567
568    #[test]
569    fn install_skill_writes_under_dot_agents_skills() {
570        let dir = tempdir().unwrap();
571        let agent = AntigravityAgent::new();
572        let scope = Scope::Local(dir.path().to_path_buf());
573        agent
574            .install_skill(&scope, &skill("alpha", "myapp"))
575            .unwrap();
576        assert!(dir.path().join(".agents/skills/alpha/SKILL.md").exists());
577        assert!(!dir.path().join(".agent/skills/alpha/SKILL.md").exists());
578        let s = fs::read_to_string(dir.path().join(".agents/skills/alpha/SKILL.md")).unwrap();
579        assert!(s.contains("name: alpha"));
580        assert!(s.contains("description: Format Git commits."));
581    }
582
583    #[test]
584    fn legacy_dot_agent_skill_status_and_uninstall_still_work() {
585        let dir = tempdir().unwrap();
586        let agent = AntigravityAgent::new();
587        let scope = Scope::Local(dir.path().to_path_buf());
588        let legacy_root = dir.path().join(".agent/skills");
589        skills_dir::install(&legacy_root, &skill("alpha", "myapp")).unwrap();
590
591        assert!(agent
592            .is_skill_installed(&scope, "alpha")
593            .expect("legacy skill status"));
594        agent.uninstall_skill(&scope, "alpha", "myapp").unwrap();
595        assert!(!dir.path().join(".agent/skills/alpha").exists());
596    }
597
598    #[test]
599    fn skill_install_idempotent() {
600        let dir = tempdir().unwrap();
601        let agent = AntigravityAgent::new();
602        let scope = Scope::Local(dir.path().to_path_buf());
603        let s = skill("alpha", "myapp");
604        agent.install_skill(&scope, &s).unwrap();
605        let r = agent.install_skill(&scope, &s).unwrap();
606        assert!(r.already_installed);
607    }
608
609    #[test]
610    fn skill_uninstall_round_trip() {
611        let dir = tempdir().unwrap();
612        let agent = AntigravityAgent::new();
613        let scope = Scope::Local(dir.path().to_path_buf());
614        agent
615            .install_skill(&scope, &skill("alpha", "myapp"))
616            .unwrap();
617        agent.uninstall_skill(&scope, "alpha", "myapp").unwrap();
618        assert!(!dir.path().join(".agents/skills/alpha").exists());
619    }
620
621    #[test]
622    fn skill_uninstall_owner_mismatch_refused() {
623        let dir = tempdir().unwrap();
624        let agent = AntigravityAgent::new();
625        let scope = Scope::Local(dir.path().to_path_buf());
626        agent
627            .install_skill(&scope, &skill("alpha", "appA"))
628            .unwrap();
629        let err = agent.uninstall_skill(&scope, "alpha", "appB").unwrap_err();
630        assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
631    }
632
633    #[test]
634    fn skill_supports_both_scopes() {
635        let agent = AntigravityAgent::new();
636        let scopes = agent.supported_skill_scopes();
637        assert!(scopes.contains(&ScopeKind::Local));
638        assert!(scopes.contains(&ScopeKind::Global));
639    }
640
641    #[test]
642    fn install_instruction_writes_under_dot_agents_rules() {
643        let dir = tempdir().unwrap();
644        let agent = AntigravityAgent::new();
645        let scope = Scope::Local(dir.path().to_path_buf());
646        agent
647            .install_instruction(&scope, &instruction("alpha", "myapp"))
648            .unwrap();
649        assert!(dir.path().join(".agents/rules/alpha.md").exists());
650        assert!(!dir.path().join(".agent/rules/alpha.md").exists());
651    }
652
653    #[test]
654    fn legacy_dot_agent_instruction_status_and_uninstall_still_work() {
655        let dir = tempdir().unwrap();
656        let agent = AntigravityAgent::new();
657        let scope = Scope::Local(dir.path().to_path_buf());
658        let legacy = instructions_dir::StandaloneLayout {
659            config_dir: dir.path().join(".agent"),
660            instruction_dir: dir.path().join(".agent/rules"),
661        };
662        instructions_dir::standalone_install(&scope, legacy, &instruction("alpha", "myapp"))
663            .unwrap();
664
665        assert!(agent
666            .is_instruction_installed(&scope, "alpha")
667            .expect("legacy instruction status"));
668        agent
669            .uninstall_instruction(&scope, "alpha", "myapp")
670            .unwrap();
671        assert!(!dir.path().join(".agent/rules/alpha.md").exists());
672    }
673
674    #[test]
675    fn rules_install_requires_rules_field() {
676        let dir = tempdir().unwrap();
677        let agent = AntigravityAgent::new();
678        let scope = Scope::Local(dir.path().to_path_buf());
679        let no_rules = HookSpec::builder("alpha")
680            .command_program("noop", [] as [&str; 0])
681            .build();
682        let err = agent.install(&scope, &no_rules).unwrap_err();
683        assert!(matches!(
684            err,
685            AgentConfigError::MissingSpecField { field: "rules", .. }
686        ));
687    }
688
689    #[test]
690    fn install_mcp_writes_dot_agent_mcp_config() {
691        let dir = tempdir().unwrap();
692        let agent = AntigravityAgent::new();
693        let scope = Scope::Local(dir.path().to_path_buf());
694        agent
695            .install_mcp(&scope, &mcp_spec("github", "myapp"))
696            .unwrap();
697        let cfg = dir.path().join(".agent/mcp_config.json");
698        let v: serde_json::Value = serde_json::from_slice(&fs::read(cfg).unwrap()).unwrap();
699        assert_eq!(
700            v["mcpServers"]["github"]["command"],
701            serde_json::json!("npx")
702        );
703    }
704
705    #[test]
706    fn uninstall_mcp_owner_mismatch_refused() {
707        let dir = tempdir().unwrap();
708        let agent = AntigravityAgent::new();
709        let scope = Scope::Local(dir.path().to_path_buf());
710        agent
711            .install_mcp(&scope, &mcp_spec("github", "appA"))
712            .unwrap();
713        let err = agent.uninstall_mcp(&scope, "github", "appB").unwrap_err();
714        assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
715    }
716}