fix desktop Antigravity ACP with macOS SOCKS proxy
This commit is contained in:
1 parent
cd8083b8f1
commit
9d301013bc
8 files changed
+132
-12
No files matched your search
@@ -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 ?? [] } : {}) };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user