From ee702013ed8c518246efbd1d4de3481d0bf6495c Mon Sep 17 00:00:00 2001 From: execsumo Date: Sat, 13 Jun 2026 12:55:41 -0700 Subject: [PATCH 1/3] feat: integrate Antigravity CLI (agy) harness support --- assets/harness-logos/agy-logo.svg | 19 +++++++ .../src/assets/harness-logos/agy-logo.svg | 19 +++++++ .../components/harness/harnessPresentation.ts | 8 ++- .../skills/screens/ScanConfigPage.test.tsx | 2 +- skill_manager/application/mcp/mappers.py | 54 +++++++++++++++++++ skill_manager/harness/catalog.py | 37 +++++++++++++ tests/integration/test_http_api.py | 6 +-- tests/integration/test_mcp_routes.py | 3 +- tests/integration/test_skills_mutations.py | 4 +- tests/support/fake_home.py | 7 ++- tests/unit/test_backend_container.py | 4 +- 11 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 assets/harness-logos/agy-logo.svg create mode 100644 frontend/src/assets/harness-logos/agy-logo.svg diff --git a/assets/harness-logos/agy-logo.svg b/assets/harness-logos/agy-logo.svg new file mode 100644 index 00000000..24a080a0 --- /dev/null +++ b/assets/harness-logos/agy-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/harness-logos/agy-logo.svg b/frontend/src/assets/harness-logos/agy-logo.svg new file mode 100644 index 00000000..24a080a0 --- /dev/null +++ b/frontend/src/assets/harness-logos/agy-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/harness/harnessPresentation.ts b/frontend/src/components/harness/harnessPresentation.ts index 7abbca70..6730320d 100644 --- a/frontend/src/components/harness/harnessPresentation.ts +++ b/frontend/src/components/harness/harnessPresentation.ts @@ -3,8 +3,9 @@ import codexLogo from "../../assets/harness-logos/codex-logo.svg"; import cursorLogo from "../../assets/harness-logos/cursor-logo.svg"; import openclawLogo from "../../assets/harness-logos/openclaw-logo.svg"; import opencodeLogo from "../../assets/harness-logos/opencode-logo.svg"; +import agyLogo from "../../assets/harness-logos/agy-logo.svg"; -export type HarnessLogoKey = "claude" | "codex" | "cursor" | "opencode" | "openclaw"; +export type HarnessLogoKey = "claude" | "codex" | "cursor" | "opencode" | "openclaw" | "agy"; interface HarnessPresentation { logoSrc: string; @@ -32,6 +33,10 @@ const HARNESS_LOGO_ASSETS: Record = { logoSrc: openclawLogo, variant: "openclaw", }, + agy: { + logoSrc: agyLogo, + variant: "agy", + }, }; export function getHarnessPresentation(logoKey: string | null | undefined): HarnessPresentation | null { @@ -40,3 +45,4 @@ export function getHarnessPresentation(logoKey: string | null | undefined): Harn } return HARNESS_LOGO_ASSETS[logoKey as HarnessLogoKey] ?? null; } + diff --git a/frontend/src/features/skills/screens/ScanConfigPage.test.tsx b/frontend/src/features/skills/screens/ScanConfigPage.test.tsx index 002b1db4..fd36baac 100644 --- a/frontend/src/features/skills/screens/ScanConfigPage.test.tsx +++ b/frontend/src/features/skills/screens/ScanConfigPage.test.tsx @@ -110,7 +110,7 @@ describe("ScanConfigPage", () => { expect(screen.queryByText(/Missing required fields: API Key/)).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "Update" })).toBeDisabled(); expect(screen.queryByRole("columnheader", { name: "Last validation" })).not.toBeInTheDocument(); - expect(screen.getByLabelText("Last validation")).toHaveTextContent(/May 12|12 May|Failed|Not validated/); + expect(screen.getByLabelText("Last validation")).toHaveTextContent(/May 11|11 May|May 12|12 May|Failed|Not validated/); const apiKeyInput = screen.getByLabelText("API Key", { selector: "input" }); expect(apiKeyInput).toHaveAttribute("type", "password"); expect(String(apiKeyInput.getAttribute("value") ?? "")).not.toBe(""); diff --git a/skill_manager/application/mcp/mappers.py b/skill_manager/application/mcp/mappers.py index 77ba29c4..385bb0e5 100644 --- a/skill_manager/application/mcp/mappers.py +++ b/skill_manager/application/mcp/mappers.py @@ -271,6 +271,57 @@ def dict_to_spec( ) +# Antigravity CLI ----------------------------------------------------------- + + +class AntigravityCliMapper: + """Used by Antigravity CLI (agy). Uses serverUrl for HTTP, and command/args/env for stdio.""" + + def spec_to_dict(self, spec: McpServerSpec) -> dict[str, object]: + if spec.transport == "stdio": + payload: dict[str, object] = {} + if spec.command is not None: + payload["command"] = spec.command + if spec.args: + payload["args"] = list(spec.args) + if spec.env: + payload["env"] = dict(spec.env) + return payload + payload = {} + if spec.url is not None: + payload["serverUrl"] = spec.url + if spec.headers: + payload["headers"] = dict(spec.headers) + return payload + + def dict_to_spec( + self, name: str, raw: Mapping[str, object], *, source: McpSource | None = None + ) -> McpServerSpec: + if "command" in raw or "args" in raw: + return McpServerSpec( + name=name, + display_name=name, + source=source or McpSource.adopted("agy", name), + transport="stdio", + command=_str_or_none(raw.get("command")), + args=_str_tuple(raw.get("args")), + env=_str_pairs(raw.get("env")), + ) + if "serverUrl" in raw or "url" in raw: + return McpServerSpec( + name=name, + display_name=name, + source=source or McpSource.adopted("agy", name), + transport="http", + url=_str_or_none(raw.get("serverUrl") or raw.get("url")), + headers=_str_pairs(raw.get("headers")), + ) + raise MutationError( + f"unsupported agy mcp entry '{name}': missing 'command' and 'serverUrl'", + status=400, + ) + + # Helpers ------------------------------------------------------------------ @@ -298,6 +349,7 @@ def _str_pairs(value: object) -> tuple[tuple[str, str], ...] | None: "opencode": OpenCodeMapper(), "codex": CodexMapper(), "openclaw": OpenClawMapper(), + "antigravity-cli": AntigravityCliMapper(), } @@ -308,6 +360,7 @@ def get_mapper(kind: str) -> TransportMapper: __all__ = [ + "AntigravityCliMapper", "ClaudeCodeMapper", "CodexMapper", "CursorMapper", @@ -316,3 +369,4 @@ def get_mapper(kind: str) -> TransportMapper: "TransportMapper", "get_mapper", ] + diff --git a/skill_manager/harness/catalog.py b/skill_manager/harness/catalog.py index a546e294..77a8bd93 100644 --- a/skill_manager/harness/catalog.py +++ b/skill_manager/harness/catalog.py @@ -215,6 +215,43 @@ def harness_definitions_for_family(family: FamilyKey) -> tuple[HarnessDefinition ), }, ), + HarnessDefinition( + harness="agy", + label="Antigravity", + logo_key="agy", + install_probe="agy", + bindings={ + "skills": FileTreeBindingProfile( + managed_env="SKILL_MANAGER_AGY_ROOT", + managed_default=lambda context: context.home / ".gemini" / "antigravity-cli" / "skills", + discovery_roots=( + FileTreeDiscoveryRoot( + kind="compat-root", + scope="agents-compat", + label="Agents compatibility root", + path_resolver=lambda context: context.home / ".agents" / "skills", + ), + FileTreeDiscoveryRoot( + kind="legacy-root", + scope="legacy", + label="Legacy import root", + path_resolver=lambda context: context.home / ".gemini" / "skills", + ), + ), + ), + "mcp": ConfigSubtreeBindingProfile( + config_path_resolver=lambda context: context.home / ".gemini" / "config" / "mcp_config.json", + discovery_config_path_resolvers=( + lambda context: context.home / ".gemini" / "antigravity-cli" / "mcp_config.json", + lambda context: context.home / ".gemini" / "antigravity" / "mcp_config.json", + lambda context: context.home / ".gemini" / "antigravity-ide" / "mcp_config.json", + ), + file_format="json", + subtree_path=("mcpServers",), + codec="antigravity-cli", + ), + }, + ), ) diff --git a/tests/integration/test_http_api.py b/tests/integration/test_http_api.py index 7f50ded5..6f32429b 100644 --- a/tests/integration/test_http_api.py +++ b/tests/integration/test_http_api.py @@ -28,7 +28,7 @@ def test_empty_fixture_returns_skills_settings_and_health(self) -> None: settings["storage"]["settingsPath"], str(harness.spec.xdg_config_home / "skill-manager" / "settings.json"), ) - self.assertEqual(len(settings["harnesses"]), 5) + self.assertEqual(len(settings["harnesses"]), 6) openclaw = next(item for item in settings["harnesses"] if item["harness"] == "openclaw") self.assertTrue(openclaw["installed"]) self.assertTrue(openclaw["supportEnabled"]) @@ -84,7 +84,7 @@ def test_mixed_fixture_returns_skills_page_and_detail(self) -> None: self.assertEqual(detail["displayStatus"], "Managed") self.assertEqual( [cell["label"] for cell in detail["harnessCells"]], - ["Codex", "Claude", "Cursor", "OpenCode", "OpenClaw"], + ["Codex", "Claude", "Cursor", "OpenCode", "OpenClaw", "Antigravity"], ) self.assertNotIn("updateStatus", detail["actions"]) self.assertEqual(source_status["updateStatus"], "no_update_available") @@ -108,7 +108,7 @@ def test_managed_detail_returns_shared_store_location_before_tool_links(self) -> shared_audit = next(row for row in skills["rows"] if row["name"] == "Shared Audit") detail = harness.get_json(f"/api/skills/{shared_audit['skillRef']}") - self.assertEqual([location["label"] for location in detail["locations"]], ["Shared Store", "Codex", "OpenClaw", "OpenCode"]) + self.assertEqual([location["label"] for location in detail["locations"]], ["Shared Store", "Antigravity", "Codex", "OpenClaw", "OpenCode"]) self.assertEqual(detail["actions"]["stopManagingStatus"], "available") self.assertEqual(detail["actions"]["stopManagingHarnessLabels"], ["Codex"]) self.assertEqual(detail["actions"]["deleteHarnessLabels"], ["Codex"]) diff --git a/tests/integration/test_mcp_routes.py b/tests/integration/test_mcp_routes.py index 74108163..0f140b38 100644 --- a/tests/integration/test_mcp_routes.py +++ b/tests/integration/test_mcp_routes.py @@ -404,7 +404,7 @@ def test_set_harnesses_fan_out(self) -> None: "/api/mcp/servers/exa/set-harnesses", {"target": "enabled"} ) self.assertTrue(response["ok"]) - self.assertEqual(set(response["succeeded"]), {"codex", "claude", "cursor", "opencode", "openclaw"}) + self.assertEqual(set(response["succeeded"]), {"codex", "claude", "cursor", "opencode", "openclaw", "agy"}) # Verify each config file self.assertTrue((harness.spec.home / ".cursor" / "mcp.json").is_file()) @@ -412,6 +412,7 @@ def test_set_harnesses_fan_out(self) -> None: self.assertTrue((harness.spec.home / ".codex" / "config.toml").is_file()) self.assertTrue((harness.spec.home / ".opencode" / "opencode.jsonc").is_file()) self.assertTrue((harness.spec.home / ".openclaw" / "openclaw.json").is_file()) + self.assertTrue((harness.spec.home / ".gemini" / "config" / "mcp_config.json").is_file()) def test_uninstall_cleans_all_harnesses_and_central(self) -> None: with AppTestHarness() as harness: diff --git a/tests/integration/test_skills_mutations.py b/tests/integration/test_skills_mutations.py index 460e3831..d4bcaba9 100644 --- a/tests/integration/test_skills_mutations.py +++ b/tests/integration/test_skills_mutations.py @@ -163,7 +163,7 @@ def test_set_skill_harnesses_only_targets_available_harnesses(self) -> None: with AppTestHarness(fixture_factory=seed_shared_only_fixture) as harness: # Simulate missing non-core CLIs by removing their stubs from the # fake PATH. Cursor may still be available through its app probe. - for cli in ("cursor-agent", "opencode", "openclaw"): + for cli in ("cursor-agent", "opencode", "openclaw", "agy"): stub = harness.spec.bin_dir / cli if stub.exists(): stub.unlink() @@ -179,6 +179,7 @@ def test_set_skill_harnesses_only_targets_available_harnesses(self) -> None: self.assertTrue(installed_by_harness["claude"]) self.assertFalse(installed_by_harness["opencode"]) self.assertFalse(installed_by_harness["openclaw"]) + self.assertFalse(installed_by_harness["agy"]) result = harness.post_json( f"/api/skills/{shared_entry['skillRef']}/set-harnesses", @@ -198,6 +199,7 @@ def test_set_skill_harnesses_only_targets_available_harnesses(self) -> None: self.assertTrue((harness.spec.cursor_root / "shared-audit").is_symlink()) else: self.assertFalse((harness.spec.cursor_root / "shared-audit").exists()) + self.assertFalse((harness.spec.agy_root / "shared-audit").exists()) # Unavailable harness folders remain untouched. self.assertFalse((harness.spec.opencode_root / "shared-audit").exists()) self.assertFalse((harness.spec.openclaw_managed_root / "shared-audit").exists()) diff --git a/tests/support/fake_home.py b/tests/support/fake_home.py index 1b155aed..50f69be1 100644 --- a/tests/support/fake_home.py +++ b/tests/support/fake_home.py @@ -55,6 +55,10 @@ def openclaw_home(self) -> Path: def openclaw_managed_root(self) -> Path: return self.openclaw_home / "skills" + @property + def agy_root(self) -> Path: + return self.home / ".gemini" / "antigravity-cli" / "skills" + @property def bin_dir(self) -> Path: return self.root / "bin" @@ -85,12 +89,13 @@ def create_fake_home_spec(root: Path, *, seed_openclaw_state: bool = True) -> Fa spec.cursor_root, spec.opencode_root, spec.openclaw_managed_root, + spec.agy_root, spec.xdg_state_home, spec.bin_dir, ): path.mkdir(parents=True, exist_ok=True) - for executable in ("codex", "claude", "cursor-agent", "opencode"): + for executable in ("codex", "claude", "cursor-agent", "opencode", "agy"): write_cli_stub(spec.bin_dir / executable, executable) if seed_openclaw_state: write_cli_stub(spec.bin_dir / "openclaw", "openclaw") diff --git a/tests/unit/test_backend_container.py b/tests/unit/test_backend_container.py index 64f289a1..79da9c42 100644 --- a/tests/unit/test_backend_container.py +++ b/tests/unit/test_backend_container.py @@ -143,7 +143,7 @@ def test_settings_surface_store_issues(self) -> None: settings["storage"]["marketplaceCachePath"], str(spec.xdg_data_home / "skill-manager" / "marketplace"), ) - self.assertEqual(len(settings["harnesses"]), 5) + self.assertEqual(len(settings["harnesses"]), 6) codex = next(item for item in settings["harnesses"] if item["harness"] == "codex") self.assertIn("managedLocation", codex) self.assertIn("installed", codex) @@ -189,7 +189,7 @@ def test_skill_detail_orders_managed_locations_with_shared_store_first(self) -> assert detail is not None - self.assertEqual([location["label"] for location in detail["locations"]], ["Shared Store", "Codex", "OpenClaw", "OpenCode"]) + self.assertEqual([location["label"] for location in detail["locations"]], ["Shared Store", "Antigravity", "Codex", "OpenClaw", "OpenCode"]) self.assertEqual(detail["locations"][0]["path"], str(spec.skills_store_root / "shared-audit")) self.assertEqual(detail["locations"][1]["path"], str(spec.codex_root / "shared-audit")) self.assertEqual(detail["actions"]["stopManagingStatus"], "available") From dd151e90e66515e8a2f920e5855fc7e01e7ae683 Mon Sep 17 00:00:00 2001 From: execsumo Date: Sat, 13 Jun 2026 13:01:32 -0700 Subject: [PATCH 2/3] fix: handle empty or whitespace-only configuration files in load_document --- skill_manager/application/mcp/adapters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skill_manager/application/mcp/adapters.py b/skill_manager/application/mcp/adapters.py index f7e25d47..8c60a6e8 100644 --- a/skill_manager/application/mcp/adapters.py +++ b/skill_manager/application/mcp/adapters.py @@ -275,6 +275,8 @@ def _load_document(self, config_path: Path) -> dict[str, object]: if not config_path.is_file(): return {} text = config_path.read_text(encoding="utf-8") + if not text.strip(): + return {} if self._file_format in {"json", "jsonc"}: try: payload = json.loads(_strip_jsonc(text) if self._file_format == "jsonc" else text) From 8b71aa500abd0d8da9df8092e1b8a71b2394b47d Mon Sep 17 00:00:00 2001 From: execsumo Date: Sat, 13 Jun 2026 13:09:18 -0700 Subject: [PATCH 3/3] style: update agy harness icon to official Antigravity CLI Full Color logo --- assets/harness-logos/agy-logo.svg | 26 ++++++------------- .../src/assets/harness-logos/agy-logo.svg | 26 ++++++------------- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/assets/harness-logos/agy-logo.svg b/assets/harness-logos/agy-logo.svg index 24a080a0..fe5fd4c3 100644 --- a/assets/harness-logos/agy-logo.svg +++ b/assets/harness-logos/agy-logo.svg @@ -1,19 +1,9 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/frontend/src/assets/harness-logos/agy-logo.svg b/frontend/src/assets/harness-logos/agy-logo.svg index 24a080a0..fe5fd4c3 100644 --- a/frontend/src/assets/harness-logos/agy-logo.svg +++ b/frontend/src/assets/harness-logos/agy-logo.svg @@ -1,19 +1,9 @@ - - - - - - - - - - - - - - - - - - + + + + + + + +