fix desktop Antigravity ACP with macOS SOCKS proxy

This commit is contained in:
tianma-if committed 2026-10-01 16:25:25 +08:00
1 parent cd8083b8f1
commit 9d301013bc
8 files changed
+132 -12

No files matched your search

+12 -6
View File
@@ -10,6 +10,7 @@ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError } fr
import { waitForChildProcessSpawn } from "./child-process-start.mjs";
import { createAcpAdapterManager } from "./acp-adapter-manager.mjs";
import { startAcpMcpBridge } from "./acp-mcp-bridge.mjs";
import { withAntigravityMacProxy } from "./antigravity-proxy.mjs";
const ADAPTERS = {
codex: { id: "codex", label: "Codex" },
@@ -687,12 +688,15 @@ export function createAcpHostRuntime(options = {}) {
],
});
const resolveCommand = (input) => {
if (input?.id === "antigravity" && typeof input.path === "string" && input.path.trim()) return resolveAcpCommand(input, commandDeps);
const resolveCommand = (input, { prepareProxy = true } = {}) => {
const adapt = (resolution) => prepareProxy && input?.id === "antigravity" && resolution.ok
? { ...resolution, command: withAntigravityMacProxy(resolution.command, commandDeps) }
: resolution;
if (input?.id === "antigravity" && typeof input.path === "string" && input.path.trim()) return adapt(resolveAcpCommand(input, commandDeps));
if (input?.id === "piAgent" && !localExecutable("pi", resolutionDeps(commandDeps))) return { ok: false, state: "not_installed" };
const managed = manager?.get(input?.id);
if (managed) return { ok: true, command: input?.id === "piAgent" ? withPiPath(managed.command, resolutionDeps(commandDeps)) : managed.command, version: managed.version, managed: true };
return resolveAcpCommand(input, commandDeps);
if (managed) return adapt({ ok: true, command: input?.id === "piAgent" ? withPiPath(managed.command, resolutionDeps(commandDeps)) : managed.command, version: managed.version, managed: true });
return adapt(resolveAcpCommand(input, commandDeps));
};
const connect = async (command, requestId, emit, signal, authMethodId, mcpServers = []) => {
@@ -754,7 +758,7 @@ export function createAcpHostRuntime(options = {}) {
listAdapters() {
return ["codex", "claudeCode", "antigravity", "openClaw", "hermesAgent", "grokBuild", "deepseekHarness", "piAgent", "workbuddyCn", "workbuddyIntl"].map((id) => {
if (installingIds.has(id)) return { ...adapterShell(id), state: "installing" };
const resolved = resolveCommand({ id });
const resolved = resolveCommand({ id }, { prepareProxy: false });
return resolved.ok
? { ...adapterShell(id), ...(latestStatus.get(id) ?? { state: "failed", detail: "not_probed" }), ...(resolved.version ? { version: resolved.version, managed: true } : {}), ...(updateFailures.has(id) ? { updateError: updateFailures.get(id) } : {}) }
: adapterFromResolution(id, resolved);
@@ -769,7 +773,9 @@ export function createAcpHostRuntime(options = {}) {
const result = await manager.install(id, async (command) => {
let connected;
try {
connected = await withHandshakeTimeout((signal) => connect(id === "piAgent" ? withPiPath(command, resolutionDeps(commandDeps)) : command, `install-${id}`, () => {}, signal), 90_000);
const prepared = id === "piAgent" ? withPiPath(command, resolutionDeps(commandDeps))
: id === "antigravity" ? withAntigravityMacProxy(command, commandDeps) : command;
connected = await withHandshakeTimeout((signal) => connect(prepared, `install-${id}`, () => {}, signal), 90_000);
return { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities };
} catch (error) {
return { ...adapterShell(id), ...failureFields(classifyAcpFailure(error)), ...(isAuthRequiredError(error) ? { authMethods: error.authMethods ?? [] } : {}) };
+6 -1
View File
@@ -580,13 +580,18 @@ describe("ACP stdio session", () => {
const { spawn } = await import("node:child_process");
const spawned = [];
const runtime = createAcpHostRuntime({
platform: "darwin",
env: {},
readProxy: () => "HTTPEnable : 1\nHTTPProxy : 127.0.0.1\nHTTPPort : 10808\nHTTPSEnable : 1\nHTTPSProxy : 127.0.0.1\nHTTPSPort : 10809\nSOCKSEnable : 1",
spawnImpl(command, args, options) {
spawned.push({ command, args, cwd: options.cwd, shell: options.shell });
spawned.push({ command, args, cwd: options.cwd, shell: options.shell, httpProxy: options.env?.HTTP_PROXY, httpsProxy: options.env?.HTTPS_PROXY });
return spawn(command, args, options);
},
});
const probed = await runtime.probeAdapter({ id: "antigravity", path: wrapper });
expect(spawned[0]?.shell).toBe(false);
expect(spawned[0]?.httpProxy).toBe("http://127.0.0.1:10808");
expect(spawned[0]?.httpsProxy).toBe("http://127.0.0.1:10809");
expect(realpathSync(spawned[0]?.command)).toBe(realpathSync(wrapper));
expect(probed.state).toBe("available");
expect(probed.promptCapabilities).toEqual({ image: true, embeddedContext: true });
@@ -0,0 +1,39 @@
import { execFileSync } from "node:child_process";
const PROXY_ENV_KEYS = ["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"];
const proxyUrl = (settings, prefix) => {
if (settings[`${prefix}Enable`] !== "1") return null;
const host = settings[`${prefix}Proxy`];
const port = Number(settings[`${prefix}Port`]);
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return null;
const bracketed = host.includes(":") ? `[${host}]` : host;
try {
const url = new URL(`http://${bracketed}:${port}`);
return url.username || url.password || url.pathname !== "/" || url.search || url.hash ? null : url.origin;
} catch {
return null;
}
};
export function antigravityMacProxyEnv({ platform = process.platform, env = process.env, readProxy = () => execFileSync("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 2_000, maxBuffer: 64 * 1024 }) } = {}) {
if (platform !== "darwin" || PROXY_ENV_KEYS.some((key) => env[key])) return null;
let output;
try { output = readProxy(); } catch { return null; }
if (typeof output !== "string") return null;
const settings = Object.fromEntries([...output.matchAll(/^\s*(\w+)\s*:\s*([^\r\n]+)\s*$/gm)].map((match) => [match[1], match[2].trim()]));
// The Antigravity ACP binary lacks python-socks. An HTTP proxy supplied in
// the environment takes precedence over macOS's system SOCKS proxy.
if (settings.SOCKSEnable !== "1") return null;
const http = proxyUrl(settings, "HTTP");
const https = proxyUrl(settings, "HTTPS");
if (!http && !https) return null;
return { ...(http ? { HTTP_PROXY: http } : {}), ...(https ? { HTTPS_PROXY: https } : {}) };
}
export function withAntigravityMacProxy(command, options) {
const proxyEnv = antigravityMacProxyEnv(options);
if (!proxyEnv) return command;
const spec = typeof command === "string" ? { command, args: [] } : command;
return { ...spec, env: { ...spec.env, ...proxyEnv } };
}
@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test";
import { antigravityMacProxyEnv, withAntigravityMacProxy } from "./antigravity-proxy.mjs";
const systemProxy = `<dictionary> {
HTTPEnable : 1
HTTPProxy : 127.0.0.1
HTTPPort : 10808
HTTPSEnable : 1
HTTPSProxy : 127.0.0.1
HTTPSPort : 10809
SOCKSEnable : 1
SOCKSProxy : 127.0.0.1
SOCKSPort : 10808
}`;
describe("Antigravity macOS proxy", () => {
test("uses the system HTTP endpoints when SOCKS would break the bundled ACP server", () => {
const options = { platform: "darwin", env: {}, readProxy: () => systemProxy };
expect(antigravityMacProxyEnv(options)).toEqual({
HTTP_PROXY: "http://127.0.0.1:10808",
HTTPS_PROXY: "http://127.0.0.1:10809",
});
expect(withAntigravityMacProxy({ command: "/tmp/agy_acp_server.par", args: [] }, options).env).toEqual(antigravityMacProxyEnv(options));
});
test("preserves explicit proxy choices and other platforms", () => {
const readProxy = () => systemProxy;
expect(antigravityMacProxyEnv({ platform: "darwin", env: { https_proxy: "socks5://localhost:9999" }, readProxy })).toBeNull();
expect(antigravityMacProxyEnv({ platform: "linux", env: {}, readProxy })).toBeNull();
expect(antigravityMacProxyEnv({ platform: "darwin", env: {}, readProxy: () => systemProxy.replace("SOCKSEnable : 1", "SOCKSEnable : 0") })).toBeNull();
});
test("does not use a SOCKS-only or malformed HTTP proxy", () => {
expect(antigravityMacProxyEnv({ platform: "darwin", env: {}, readProxy: () => "SOCKSEnable : 1\nSOCKSProxy : localhost\nSOCKSPort : 1080" })).toBeNull();
expect(antigravityMacProxyEnv({ platform: "darwin", env: {}, readProxy: () => systemProxy.replaceAll("127.0.0.1", "user@untrusted.example") })).toBeNull();
});
});
@@ -9,6 +9,7 @@ import {
AI_SIDEBAR_ADAPTER_KEY,
AI_SIDEBAR_ADAPTER_PATH_KEY,
AI_SIDEBAR_SOURCE_KEY,
displayedDesktopAcpAdapter,
listDesktopAcpAdapters,
installDesktopAcpAdapter,
authenticateDesktopAcpAdapter,
@@ -116,10 +117,7 @@ const DesktopAcpAgentCardBody = ({ bridge }: { bridge: boolean }) => {
};
}, []);
const listedCurrent = listed.find((adapter) => adapter.id === adapterId);
const shown = listedCurrent?.state === "installing" || (listedCurrent?.managed && (!probed?.managed || listedCurrent.version !== probed.version))
? listedCurrent
: probed?.id === adapterId ? probed : listedCurrent;
const shown = displayedDesktopAcpAdapter({ id: adapterId, path: adapterPath, listed, probed });
const status = t(statusKey(shown, probing));
const tone = getStatusTone({
probing,
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "bun:test";
import { displayedDesktopAcpAdapter } from "./desktop-acp.ts";
test("a checked custom Antigravity executable takes priority over the managed connector status", () => {
const managed = { id: "antigravity", label: "Antigravity", state: "failed", version: "1.2.1", managed: true };
const custom = { id: "antigravity", label: "Antigravity", state: "available" };
const input = { id: "antigravity", path: "/tmp/agy-acp", listed: [managed], probed: custom };
expect(displayedDesktopAcpAdapter(input)).toBe(custom);
expect(displayedDesktopAcpAdapter({ ...input, probed: null })).toBeUndefined();
expect(displayedDesktopAcpAdapter({ ...input, path: "" })).toBe(managed);
});
test("a newly installed managed connector takes priority over an older probe", () => {
const current = { id: "antigravity", label: "Antigravity", state: "available", version: "1.2.2", managed: true };
const previous = { id: "antigravity", label: "Antigravity", state: "failed", version: "1.2.1", managed: true };
expect(displayedDesktopAcpAdapter({ id: "antigravity", path: "", listed: [current], probed: previous })).toBe(current);
});
+18
View File
@@ -18,6 +18,24 @@ export type DesktopAcpAdapter = {
authMethods?: Array<{ id: string; name: string }>;
};
export const displayedDesktopAcpAdapter = ({
id,
path,
listed,
probed,
}: {
id: DesktopAcpAdapterId;
path: string;
listed: DesktopAcpAdapter[];
probed: DesktopAcpAdapter | null;
}): DesktopAcpAdapter | undefined => {
const current = listed.find((adapter) => adapter.id === id);
const checked = probed?.id === id ? probed : undefined;
if (id === "antigravity" && path.trim()) return checked;
if (current?.state === "installing" || (current?.managed && (!checked?.managed || current.version !== checked.version))) return current;
return checked ?? current;
};
export type DesktopAcpAttachment = {
filename: string;
mediaType: string;
+1 -1
View File
@@ -63,7 +63,7 @@
"release": "bun scripts/release.mjs",
"publish:stores": "bun scripts/store-delivery.mjs",
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
"test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/wechat-chat-archive.test.mjs apps/desktop/src/main/wechat-share-import.test.mjs apps/desktop/src/main/share-extension-registration.test.mjs apps/desktop/src/main/windows-share-menu.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/ai-direct.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-hibernate.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/screenshot-capture.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/staged-resource-alias.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-workspace-restore.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/screenshot-import.test.mjs apps/web/src/lib/wechat-chat-import.test.mjs apps/web/src/lib/shared-file-import.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun test --cwd apps/desktop src/main/acp-adapter-manager.test.mjs src/main/acp-host.test.mjs src/main/acp-mcp-bridge.test.mjs && bun run test:desktop:sidecar",
"test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/wechat-chat-archive.test.mjs apps/desktop/src/main/wechat-share-import.test.mjs apps/desktop/src/main/share-extension-registration.test.mjs apps/desktop/src/main/windows-share-menu.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/ai-direct.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-hibernate.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/screenshot-capture.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/staged-resource-alias.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-workspace-restore.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-acp.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/screenshot-import.test.mjs apps/web/src/lib/wechat-chat-import.test.mjs apps/web/src/lib/shared-file-import.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun test --cwd apps/desktop src/main/acp-adapter-manager.test.mjs src/main/acp-host.test.mjs src/main/antigravity-proxy.test.mjs src/main/acp-mcp-bridge.test.mjs && bun run test:desktop:sidecar",
"verify:desktop-renderer": "bun scripts/verify-desktop-renderer.mjs",
"verify:renderer-origin-migration": "bun run --cwd apps/desktop verify-origin-migration",
"verify:desktop-cross-version-startup": "bun scripts/verify-desktop-cross-version-startup.mjs",