dev/verify_agy_gemini_dir.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Verify the antigravity-native (agy) CLI launch is scoped to a per-session Gemini dir.
Run this on macOS (or Linux) before and after the #1477 / #1194 fix. It answers
four questions without a server, a runner, or a real agy launch:
1. Does the CLI launch pass ``--gemini_dir=<per-session dir>``?
2. Does the Omnigent MCP relay config land in that dir (so agy gets ``sys_*``)?
3. Is ``HOME`` left real (so a keyring/Keychain-backed OAuth token still resolves)?
4. Is the user's real ``~/.gemini`` left byte-for-byte untouched?
Your real ``~/.gemini`` is never written: the home directory is redirected to a
throwaway copy first, so this is safe to run on a signed-in machine.
Usage::
.venv/bin/python dev/verify_agy_gemini_dir.py
Exit code 0 = every check passed (post-fix). Non-zero = at least one failed, with
the failure printed and the sandbox kept for inspection. A passing run then prints
the manual steps for the part only a real agy can prove: that an agy launched this
way is actually signed in.
"""
from __future__ import annotations
import asyncio
import json
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Any
import httpx
import omnigent.harnesses.antigravity_native.bridge as bridge
import omnigent.harnesses.antigravity_native.main as agy_cli
class _StubClient:
"""Answers the terminal create/patch calls without a server."""
async def post(self, url: str, **_kwargs: Any) -> httpx.Response:
return httpx.Response(
200,
json={"id": "terminal_antigravity_main", "metadata": {}},
request=httpx.Request("POST", url),
)
async def patch(self, url: str, **_kwargs: Any) -> httpx.Response:
return httpx.Response(200, json={}, request=httpx.Request("PATCH", url))
async def get(self, url: str, **_kwargs: Any) -> httpx.Response:
return httpx.Response(200, json={}, request=httpx.Request("GET", url))
def _snapshot(root: Path) -> dict[str, str]:
"""Map every file under *root* to its contents, for an exact-equality diff."""
return {
str(path.relative_to(root)): path.read_text(encoding="utf-8", errors="replace")
for path in sorted(root.rglob("*"))
if path.is_file()
}
async def _run(sandbox: Path) -> tuple[list[str], dict[str, str], Path, set[str]]:
"""Drive the real CLI launch path against a fake home; return what agy would get."""
fake_home = sandbox / "home"
real_gemini = fake_home / ".gemini"
(real_gemini / "antigravity-cli").mkdir(parents=True)
(real_gemini / "config").mkdir(parents=True)
# Stand in for a signed-in user who also has their own agy MCP config + settings.
(real_gemini / "oauth_creds.json").write_text('{"access_token": "real-token"}')
(real_gemini / "google_accounts.json").write_text('{"active": "user@example.com"}')
# An opaque placeholder, not a real model id: this only has to be a setting of
# the user's that the launch must leave alone.
(real_gemini / "antigravity-cli" / "settings.json").write_text('{"model": "user-picked"}')
(real_gemini / "config" / "mcp_config.json").write_text('{"mcpServers": {"mine": {}}}')
# Redirect HOME so the real ~/.gemini is untouched, and re-point the two module
# constants that captured Path.home() at import time (a Path.home patch alone
# leaves them aimed at the real home).
Path.home = classmethod(lambda _cls: fake_home) # type: ignore[method-assign]
bridge.AGY_APP_DATA_DIR = real_gemini / "antigravity-cli"
bridge._AGY_ONBOARDING_MARKER = bridge.AGY_APP_DATA_DIR / "cache" / "onboarding.json"
bridge._BRIDGE_ROOT = sandbox / "omnigent" / "antigravity-native"
# agy itself is never launched: stub the binary resolution and the terminal call.
agy_cli.build_agy_launch = lambda **kwargs: ( # type: ignore[assignment]
["agy", *(("--model", kwargs["model"]) if kwargs.get("model") else ())],
{},
)
captured: dict[str, Any] = {}
async def _fake_terminal(
_client: Any, _session_id: str, *, argv: list[str], env: dict[str, str], **_kw: Any
) -> Any:
captured["argv"], captured["env"] = list(argv), dict(env)
return agy_cli.LaunchedAntigravityTerminal(
terminal_id="terminal_antigravity_main", tmux_socket=None, tmux_target=None
)
agy_cli._launch_antigravity_terminal = _fake_terminal # type: ignore[assignment]
bridge_id = "verify_agy_gemini_dir"
bridge_dir = bridge.prepare_bridge_dir(bridge_id)
before = _snapshot(real_gemini)
await agy_cli._launch_and_record(
_StubClient(), # type: ignore[arg-type]
session_id="conv_verify",
bridge_id=bridge_id,
conversation_id="agy_conv_placeholder",
resume=False,
antigravity_args=(),
command="agy",
model=None,
permission_mode=None,
headless=True,
startup_progress=None,
)
after = _snapshot(real_gemini)
changed = {name for name in set(before) | set(after) if before.get(name) != after.get(name)}
return captured["argv"], captured["env"], bridge_dir, changed
def main() -> int:
# Kept on disk until every check passes, so a failure can be inspected.
sandbox = Path(tempfile.mkdtemp(prefix="agy-verify-"))
argv, env, bridge_dir, changed = asyncio.run(_run(sandbox))
iso_gemini = bridge.agy_gemini_dir(bridge_dir)
relay_config = iso_gemini / "config" / "mcp_config.json"
gemini_dir_flag = next((arg for arg in argv if arg.startswith("--gemini_dir=")), None)
failures: list[str] = []
print("agy argv:", " ".join(argv))
print("agy env overrides:", sorted(env) or "(none)")
print()
# 1. The flag agy needs to read a per-session config at all.
if gemini_dir_flag == f"--gemini_dir={iso_gemini}":
print(f"PASS --gemini_dir points at the per-session dir\n {iso_gemini}")
else:
failures.append(
f"--gemini_dir missing or wrong (got {gemini_dir_flag!r}); agy will read the "
"user's real ~/.gemini, so it sees no Omnigent relay and no sys_* tools"
)
# 2. The relay config, in the dir the flag points at.
if relay_config.is_file():
tools = json.loads(relay_config.read_text())["mcpServers"]["omnigent"]["enabledTools"]
print(f"PASS relay mcp_config.json written ({len(tools)} tools enabled)")
else:
failures.append(f"no relay mcp_config.json at {relay_config}")
# 3. HOME must stay real or a Keychain/keyring token cannot be unlocked.
if "HOME" not in env:
print("PASS HOME not overridden (keyring / macOS Keychain auth still resolves)")
else:
failures.append(f"HOME overridden to {env['HOME']!r}; breaks macOS Keychain auth")
# 4. The user's own agy config must survive untouched.
if not changed:
print("PASS the real ~/.gemini is byte-for-byte unchanged")
else:
failures.append(f"the real ~/.gemini was modified: {sorted(changed)}")
print()
if failures:
print(f"FAILED ({len(failures)}):")
for item in failures:
print(" -", item)
print(f"\nsandbox kept for inspection: {sandbox}")
return 1
shutil.rmtree(sandbox, ignore_errors=True)
print("All checks passed.\n")
print("Still worth confirming by hand on macOS (only a real agy can prove auth):")
print(" 1. agy --version # installed, and you are signed in")
print(" 2. agy --gemini_dir=<a fresh empty dir> --help # the flag is accepted")
print(" 3. omnigent antigravity --server <url> # then in the agy TUI: /mcp")
print(" Expect '✓ omnigent' with sys_* tools, and NO login prompt.")
return 0
if __name__ == "__main__":
sys.exit(main())