Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions assets/harness-logos/agy-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions frontend/src/assets/harness-logos/agy-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion frontend/src/components/harness/harnessPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -32,6 +33,10 @@ const HARNESS_LOGO_ASSETS: Record<HarnessLogoKey, HarnessPresentation> = {
logoSrc: openclawLogo,
variant: "openclaw",
},
agy: {
logoSrc: agyLogo,
variant: "agy",
},
};

export function getHarnessPresentation(logoKey: string | null | undefined): HarnessPresentation | null {
Expand All @@ -40,3 +45,4 @@ export function getHarnessPresentation(logoKey: string | null | undefined): Harn
}
return HARNESS_LOGO_ASSETS[logoKey as HarnessLogoKey] ?? null;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down
2 changes: 2 additions & 0 deletions skill_manager/application/mcp/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions skill_manager/application/mcp/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ------------------------------------------------------------------


Expand Down Expand Up @@ -298,6 +349,7 @@ def _str_pairs(value: object) -> tuple[tuple[str, str], ...] | None:
"opencode": OpenCodeMapper(),
"codex": CodexMapper(),
"openclaw": OpenClawMapper(),
"antigravity-cli": AntigravityCliMapper(),
}


Expand All @@ -308,6 +360,7 @@ def get_mapper(kind: str) -> TransportMapper:


__all__ = [
"AntigravityCliMapper",
"ClaudeCodeMapper",
"CodexMapper",
"CursorMapper",
Expand All @@ -316,3 +369,4 @@ def get_mapper(kind: str) -> TransportMapper:
"TransportMapper",
"get_mapper",
]

37 changes: 37 additions & 0 deletions skill_manager/harness/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
},
),
)


Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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")
Expand All @@ -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"])
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/test_mcp_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,14 +404,15 @@ 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())
self.assertTrue((harness.spec.home / ".claude.json").is_file())
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:
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/test_skills_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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",
Expand All @@ -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())
Expand Down
7 changes: 6 additions & 1 deletion tests/support/fake_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_backend_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down