-
Notifications
You must be signed in to change notification settings - Fork 3
antigravity-rewrite #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maker-or
wants to merge
5
commits into
main
Choose a base branch
from
pipper/antigravity-rewrite
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,255
−127
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ae0bdb8
Integrate official Antigravity ACP binary and harden connection lifec…
maker-or 9a93894
Preserve Antigravity snapshots and harden install and restore
maker-or a555ff3
Harden Antigravity snapshot-only launch and installer lock
maker-or 4b29806
Merge remote-tracking branch 'origin/main' into pipper/antigravity-re…
maker-or 7c4f2a6
Merge remote-tracking branch 'origin/main' into pipper/antigravity-re…
maker-or File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Google Antigravity ACP integration | ||
|
|
||
| Updated 24 September 2026. After testing an installed-CLI bridge, the user chose Google's official ACP server. The CLI's headless mode soft-denied tool actions that needed approval. The official server speaks ACP directly, allowing Pipper to display and answer permission requests. [Google's Zed setup](https://antigravity.google/docs/ide/extensions/zed/), [ACP registry entry](https://raw.githubusercontent.com/agentclientprotocol/registry/main/antigravity-acp/agent.json), [CLI headless permissions](https://antigravity.google/docs/cli/headless/). | ||
|
|
||
| ## Implementation | ||
|
|
||
| - Pipper fetches the pinned Google ACP server version 1.2.1 on first use into a versioned user cache. The application bundle does not contain the Google server. macOS ARM64 and Windows x64 correspond to Pipper's current packaged targets; each archive has a release-pinned SHA-256 and exact expected file list. Installation uses a temporary directory, checksum verification, and atomic promotion. Concurrent callers share the install, with a cross-process lock. The macOS archive is 107 MiB compressed and includes the server and its `localharness_external` sibling. | ||
| - The existing ACP connection lifecycle, session routing, transcript rendering, MCP attachment, and client tool handlers now talk directly to Google's server. The first-party CLI bridge and npm adapter launch are removed. Permission requests enter Pipper's existing approval UI. An unanswered approval now cancels after two minutes instead of auto-allowing. | ||
| - Pipper offers the server's advertised Google OAuth, enterprise OAuth, Gemini API-key, and agent-platform authentication methods in setup. The CLI's cached credentials did not authenticate the ACP server in the local handshake, so users must complete ACP sign-in. The agent decides how each method obtains credentials; Pipper does not read credential files. | ||
| - Antigravity restoration errors preserve the thread's existing Pipper snapshot and session ID rather than silently starting a replacement. Prior CLI-bridge session IDs are explicitly identified as incompatible with the official server. A new Antigravity thread is required to continue those conversations. | ||
| - The app and public agent setup copy now describe the official server rather than the installed CLI. | ||
|
|
||
| ## Verification and limits | ||
|
|
||
| The real macOS ARM64 server archive was downloaded and its SHA-256 recorded. A local install exercised checksum verification, extraction of both required files, atomic cache promotion, and a repeated cache hit. The verified files were moved to this machine's Pipper cache. The official executable negotiated ACP protocol version 1, advertised load/resume and four auth methods, and returned `auth_required` for `session/new` before ACP sign-in. Pipper's actual handshake probe classified this as `needs-auth` and returned those methods. | ||
|
|
||
| The earlier authenticated text-prompt test applied to the CLI bridge and does not prove an authenticated official-server prompt. A user must complete ACP sign-in to validate live prompts and interactive tool approvals. Windows installation and permission behavior require a Windows runtime check. Existing npm-adapter and CLI-bridge sessions remain displayable in Pipper but are not imported into Google's server. | ||
|
|
||
| Repository checks after the switch: app build, Electron TypeScript check, lint, React Doctor (100/100), and the full 443-test suite passed. The marketing site build was unavailable in this worktree because its separate `astro` dependency is not installed. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { readFileSync, writeFileSync } from "node:fs"; | ||
| import { mkdtemp, mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { afterEach, expect, test, vi } from "vitest"; | ||
| import { | ||
| antigravityCacheRoot, | ||
| antigravityRelease, | ||
| ensureAntigravityInstalled, | ||
| type AntigravityRelease, | ||
| } from "./antigravity-official.ts"; | ||
| import { getAgentDescriptor, resolveAgentSpawn } from "./registry.ts"; | ||
|
|
||
| const originalPlatform = process.platform; | ||
| let temporary: string | null = null; | ||
| afterEach(async () => { | ||
| vi.unstubAllEnvs(); | ||
| vi.unstubAllGlobals(); | ||
| Object.defineProperty(process, "platform", { value: originalPlatform }); | ||
| if (temporary) await rm(temporary, { recursive: true, force: true }); | ||
| temporary = null; | ||
| }); | ||
|
|
||
| function zipAvailable(): boolean { | ||
| try { | ||
| execFileSync("zip", ["-v"], { stdio: "ignore" }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** Build a real zip fixture with the given entries, then return its bytes. */ | ||
| function makeFixtureZip(directory: string, files: Record<string, string>): Buffer { | ||
| for (const [name, content] of Object.entries(files)) { | ||
| writeFileSync(join(directory, name), content); | ||
| } | ||
| execFileSync("zip", ["-q", "fixture.zip", ...Object.keys(files)], { cwd: directory }); | ||
| return readFileSync(join(directory, "fixture.zip")); | ||
| } | ||
|
|
||
| function fixtureRelease(archive: Buffer): AntigravityRelease { | ||
| return { | ||
| archive: "https://dl.google.com/agy-extensions/releases/fixture.zip", | ||
| sha256: createHash("sha256").update(archive).digest("hex"), | ||
| executable: "agy_acp_server.exe", | ||
| files: ["agy_acp_server.exe", "localharness_external.exe"], | ||
| args: [], | ||
| }; | ||
| } | ||
|
|
||
| function stubArchiveFetch(archive: Buffer) { | ||
| const fetch = vi.fn(async () => { | ||
| const response = new Response(new Uint8Array(archive)); | ||
| Object.defineProperty(response, "url", { | ||
| value: "https://dl.google.com/agy-extensions/releases/fixture.zip", | ||
| }); | ||
| return response; | ||
| }); | ||
| vi.stubGlobal("fetch", fetch); | ||
| return fetch; | ||
| } | ||
|
|
||
| test.skipIf(!antigravityRelease())( | ||
| "reuses a verified cached server without a download", | ||
| async () => { | ||
| const release = antigravityRelease()!; | ||
| temporary = await mkdtemp(join(tmpdir(), "pipper-agy-acp-test-")); | ||
| vi.stubEnv("PIPPER_ACP_AGENT_CACHE", temporary); | ||
| const root = antigravityCacheRoot(); | ||
| await mkdir(root, { recursive: true }); | ||
| const sizes: Record<string, number> = {}; | ||
| for (const file of release.files) { | ||
| await writeFile(join(root, file), "fixture"); | ||
| sizes[file] = (await stat(join(root, file))).size; | ||
| } | ||
| await writeFile( | ||
| join(root, "install.json"), | ||
| JSON.stringify({ version: "1.2.1", sha256: release.sha256, files: sizes }), | ||
| ); | ||
| const fetch = vi.fn(() => Promise.reject(new Error("Unexpected download"))); | ||
| vi.stubGlobal("fetch", fetch); | ||
| const executable = await ensureAntigravityInstalled(); | ||
| expect(executable).toBe(join(root, release.executable)); | ||
| expect(resolveAgentSpawn(getAgentDescriptor("antigravity-acp")!).command).toBe(executable); | ||
| expect(fetch).not.toHaveBeenCalled(); | ||
| }, | ||
| ); | ||
|
|
||
| test.skipIf(!antigravityRelease())("repairs a cache whose executable was truncated", async () => { | ||
| const release = antigravityRelease()!; | ||
| temporary = await mkdtemp(join(tmpdir(), "pipper-agy-acp-test-")); | ||
| vi.stubEnv("PIPPER_ACP_AGENT_CACHE", temporary); | ||
| const root = antigravityCacheRoot(); | ||
| await mkdir(root, { recursive: true }); | ||
| const sizes: Record<string, number> = {}; | ||
| for (const file of release.files) { | ||
| await writeFile(join(root, file), "fixture"); | ||
| sizes[file] = (await stat(join(root, file))).size; | ||
| } | ||
| await writeFile( | ||
| join(root, "install.json"), | ||
| JSON.stringify({ version: "1.2.1", sha256: release.sha256, files: sizes }), | ||
| ); | ||
| // A present-but-damaged executable must not pass the cache check: the | ||
| // truncated file fails the recorded size and triggers a reinstall. | ||
| await writeFile(join(root, release.executable), "trunc"); | ||
| const fetch = vi.fn(() => Promise.reject(new Error("Unexpected download"))); | ||
| vi.stubGlobal("fetch", fetch); | ||
| await expect(ensureAntigravityInstalled()).rejects.toThrow("Unexpected download"); | ||
| expect(fetch).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| test.skipIf(!antigravityRelease())( | ||
| "reclaims a stale installer lock instead of waiting it out", | ||
| async () => { | ||
| temporary = await mkdtemp(join(tmpdir(), "pipper-agy-acp-test-")); | ||
| vi.stubEnv("PIPPER_ACP_AGENT_CACHE", temporary); | ||
| const root = antigravityCacheRoot(); | ||
| await mkdir(join(root, ".."), { recursive: true }); | ||
| const lockPath = `${root}.lock`; | ||
| await writeFile(lockPath, "dead-installer-nonce"); | ||
| const stale = new Date(Date.now() - 10 * 60_000); | ||
| await utimes(lockPath, stale, stale); | ||
| const fetch = vi.fn(() => Promise.reject(new Error("Unexpected download"))); | ||
| vi.stubGlobal("fetch", fetch); | ||
| // Reaching the download proves the stale lock was reclaimed; otherwise the | ||
| // installer would sit on its 6-minute wait. | ||
| await expect(ensureAntigravityInstalled()).rejects.toThrow("Unexpected download"); | ||
| expect(fetch).toHaveBeenCalledTimes(1); | ||
| }, | ||
| ); | ||
|
|
||
| test.skipIf(process.platform !== "darwin" || !zipAvailable())( | ||
| "installs through the Windows tar layout", | ||
| async () => { | ||
| // Exercise the win32 branch (tar -tf / tar -xOf, no chmod) with macOS | ||
| // bsdtar, which reads the same zip archive the Windows release ships. | ||
| Object.defineProperty(process, "platform", { value: "win32" }); | ||
| temporary = await mkdtemp(join(tmpdir(), "pipper-agy-acp-test-")); | ||
| vi.stubEnv("PIPPER_ACP_AGENT_CACHE", temporary); | ||
| const fixtureDir = join(temporary, "fixture"); | ||
| await mkdir(fixtureDir, { recursive: true }); | ||
| const archive = makeFixtureZip(fixtureDir, { | ||
| "agy_acp_server.exe": "server-binary", | ||
| "localharness_external.exe": "harness-binary", | ||
| }); | ||
| const release = fixtureRelease(archive); | ||
| const fetch = stubArchiveFetch(archive); | ||
|
|
||
| const executable = await ensureAntigravityInstalled({ release }); | ||
| const root = antigravityCacheRoot(); | ||
| expect(executable).toBe(join(root, "agy_acp_server.exe")); | ||
| expect(await readFile(executable, "utf8")).toBe("server-binary"); | ||
| expect(await readFile(join(root, "localharness_external.exe"), "utf8")).toBe("harness-binary"); | ||
| // The promoted cache carries per-file sizes and is reused without a fetch. | ||
| const marker = JSON.parse(await readFile(join(root, "install.json"), "utf8")); | ||
| expect(marker.files["agy_acp_server.exe"]).toBe("server-binary".length); | ||
| await expect(ensureAntigravityInstalled({ release })).resolves.toBe(executable); | ||
| expect(fetch).toHaveBeenCalledTimes(1); | ||
| }, | ||
| ); | ||
|
|
||
| test.skipIf(!antigravityRelease())("rejects an archive with the wrong checksum", async () => { | ||
| temporary = await mkdtemp(join(tmpdir(), "pipper-agy-acp-test-")); | ||
| vi.stubEnv("PIPPER_ACP_AGENT_CACHE", temporary); | ||
| const fetch = vi.fn(async () => { | ||
| const response = new Response("not the pinned archive"); | ||
| Object.defineProperty(response, "url", { value: antigravityRelease()!.archive }); | ||
| return response; | ||
| }); | ||
| vi.stubGlobal("fetch", fetch); | ||
| await expect(ensureAntigravityInstalled()).rejects.toThrow("checksum"); | ||
| expect(fetch).toHaveBeenCalledTimes(1); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session/newin a fresh server process. Both paths are needed by the supported Windows target and the new setup flow; without success-path coverage, archive-layout and cross-process sign-in regressions can go undetected.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!