@estebanforge/pi-antigravity-bridge 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +12 -14
- package/extensions/index.ts +115 -369
- package/package.json +1 -1
- package/src/acp/connection.ts +56 -109
- package/src/acp/driver.ts +38 -62
- package/src/acp/events.ts +17 -11
- package/src/acp/home.ts +95 -0
- package/src/acp/jsonrpc.ts +6 -1
- package/src/acp/setup.ts +71 -390
- package/src/agents.ts +1 -1
- package/src/agy-install.ts +85 -0
- package/src/agy-paths.ts +3 -6
- package/src/agy-setup.ts +72 -0
- package/src/agy-version.ts +3 -4
- package/src/ask-tool.ts +3 -3
- package/src/config.ts +17 -19
- package/src/driver-types.ts +3 -5
- package/src/engine-picker.ts +8 -12
- package/src/mcp-registration.ts +1 -1
- package/src/mcp-server.ts +5 -5
- package/src/provider.ts +4 -4
- package/src/queued-messages.ts +7 -26
- package/src/acp/auth.ts +0 -119
- package/src/acp/browser-capture.ts +0 -104
package/src/acp/auth.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { AcpConnection } from "./connection.js";
|
|
2
|
-
import { hasAcpToken, writeAcpAuthVerified } from "./setup.js";
|
|
3
|
-
|
|
4
|
-
// Explicit sign-in (/agy auth): spawn a short-lived server, send
|
|
5
|
-
// `authenticate` (the RPC that fires the loopback listener + browser OAuth
|
|
6
|
-
// flow), and wait for the token to land in the shared acp_token.json. A
|
|
7
|
-
// dedicated process keeps the minutes-scale login ritual away from live
|
|
8
|
-
// turns; the next driver connection picks the token up. The real server
|
|
9
|
-
// blocks until the browser round-trip completes; the observed window is
|
|
10
|
-
// minutes-scale (one flow timed out at ~8.5 minutes), so the timeout is
|
|
11
|
-
// generous.
|
|
12
|
-
//
|
|
13
|
-
// Token-absent after an OK reply is NOT always a failure: server 1.1.1 can
|
|
14
|
-
// authenticate from legacy ~/.gemini credential artifacts and never writes
|
|
15
|
-
// acp_token.json (observed on darwin/arm64 2026-10-13). A live session/new
|
|
16
|
-
// probe separates that from a hollow accept: probe success = signed in
|
|
17
|
-
// (recorded in the bridge-side verified marker), probe failure = the server
|
|
18
|
-
// holds no usable credentials.
|
|
19
|
-
|
|
20
|
-
export const AUTH_TIMEOUT_MS = 10 * 60_000;
|
|
21
|
-
|
|
22
|
-
export interface AcpAuthOptions {
|
|
23
|
-
bin: string;
|
|
24
|
-
binArgs?: string[];
|
|
25
|
-
cwd?: string;
|
|
26
|
-
extraEnv?: Record<string, string>;
|
|
27
|
-
authUrlFile?: string;
|
|
28
|
-
/** Directory holding acp_token.json. Default: ~/.gemini/antigravity-acp.
|
|
29
|
-
* Tests point this at a fixture dir; production callers use the default. */
|
|
30
|
-
tokenDir?: string;
|
|
31
|
-
/** The server is expected to have written acp_token.json by the time
|
|
32
|
-
* authenticate replies, but write-after-reply is unverified; after a
|
|
33
|
-
* successful reply the run polls for the token up to this long before
|
|
34
|
-
* SIGTERM, so a laggard write is never killed mid-flight. A token that
|
|
35
|
-
* never appears is verified through a live session probe instead of
|
|
36
|
-
* failing outright. */
|
|
37
|
-
tokenGraceMs?: number;
|
|
38
|
-
/** Bridge-side verified-sign-in marker. Written on every verified
|
|
39
|
-
* success (token file or live session probe). Default is the real
|
|
40
|
-
* bridge path; tests point this at a fixture. */
|
|
41
|
-
authVerifiedFile?: string;
|
|
42
|
-
timeoutMs?: number;
|
|
43
|
-
log?: (msg: string, data?: unknown) => void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface AcpAuthResult {
|
|
47
|
-
ok: boolean;
|
|
48
|
-
/** Present when verified through the session probe: the server
|
|
49
|
-
* authenticated from existing credentials without writing a local
|
|
50
|
-
* token file. Absent on the normal token-file path and on failure. */
|
|
51
|
-
note?: string;
|
|
52
|
-
/** Failure reason (spawn, timeout, -32000 family, concurrent run). Absent when ok. */
|
|
53
|
-
error?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Live credential probe for the token-absent branch: a server with usable
|
|
57
|
-
* credentials answers session/new; a hollow accept starts (or wants) a
|
|
58
|
-
* browser flow and the request times out or errors. Closes the probe
|
|
59
|
-
* session; the caller kills the connection either way. */
|
|
60
|
-
async function probeUsableCredentials(conn: AcpConnection, cwd?: string): Promise<boolean> {
|
|
61
|
-
try {
|
|
62
|
-
const s = await conn.newSession(cwd ?? process.cwd());
|
|
63
|
-
await conn.closeSession(s.sessionId).catch(() => {});
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let authInFlight: Promise<AcpAuthResult> | null = null;
|
|
71
|
-
|
|
72
|
-
export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
73
|
-
const conn = new AcpConnection({
|
|
74
|
-
bin: opts.bin,
|
|
75
|
-
binArgs: opts.binArgs,
|
|
76
|
-
cwd: opts.cwd ?? process.cwd(),
|
|
77
|
-
extraEnv: opts.extraEnv,
|
|
78
|
-
authUrlFile: opts.authUrlFile,
|
|
79
|
-
log: opts.log ?? (() => {}),
|
|
80
|
-
onUpdate: () => {},
|
|
81
|
-
onExit: () => {},
|
|
82
|
-
});
|
|
83
|
-
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
84
|
-
const grace = opts.tokenGraceMs ?? 5_000;
|
|
85
|
-
const run = async (): Promise<AcpAuthResult> => {
|
|
86
|
-
try {
|
|
87
|
-
await conn.start();
|
|
88
|
-
await conn.request("authenticate", { methodId: "oauth-personal" }, opts.timeoutMs ?? AUTH_TIMEOUT_MS);
|
|
89
|
-
const deadline = Date.now() + grace;
|
|
90
|
-
while (!hasAcpToken(opts.tokenDir) && Date.now() < deadline) await sleep(200);
|
|
91
|
-
if (!hasAcpToken(opts.tokenDir)) {
|
|
92
|
-
// authenticate replied but no token file: either the server
|
|
93
|
-
// authenticated from artifacts outside antigravity-acp, or the
|
|
94
|
-
// reply is hollow. A live session/new separates the two.
|
|
95
|
-
if (!(await probeUsableCredentials(conn, opts.cwd))) {
|
|
96
|
-
return {
|
|
97
|
-
ok: false,
|
|
98
|
-
error: `the server accepted the sign-in but holds no usable credentials (session probe failed; token file absent in ${opts.tokenDir ?? "~/.gemini/antigravity-acp"}). Finish the Google sign-in if a browser opened, then retry /agy auth.`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
102
|
-
return { ok: true, note: "the server authenticated from existing credentials; no browser sign-in was needed" };
|
|
103
|
-
}
|
|
104
|
-
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
105
|
-
return { ok: true };
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
108
|
-
} finally {
|
|
109
|
-
conn.kill();
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
// One sign-in at a time: two concurrent runs would race the OAuth loopback
|
|
113
|
-
// and the token write. A second invocation fails fast instead.
|
|
114
|
-
if (authInFlight) return Promise.resolve({ ok: false, error: "a sign-in is already running; wait for it to finish" });
|
|
115
|
-
authInFlight = run();
|
|
116
|
-
return authInFlight.finally(() => {
|
|
117
|
-
authInFlight = null;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
// The ACP server hands the Google OAuth URL ONLY to the browser-open call:
|
|
6
|
-
// nothing on stdout or stderr, headless or not (verified per
|
|
7
|
-
// docs/ACP-PROTOCOL-REFERENCE.md). The capture makes the URL visible:
|
|
8
|
-
// BROWSER points at a wrapper that appends the URL to a record file and then
|
|
9
|
-
// execs the real opener, and the connection watches the record file to log
|
|
10
|
-
// "auth-url". Local users keep the automatic browser open; over SSH the user
|
|
11
|
-
// copies the URL and forwards the redirect port.
|
|
12
|
-
|
|
13
|
-
export interface AuthUrlCapture {
|
|
14
|
-
/** Spawn env for the ACP server (BROWSER -> wrapper script). */
|
|
15
|
-
browserEnv: Record<string, string>;
|
|
16
|
-
/** Record file the connection watches. */
|
|
17
|
-
file: string;
|
|
18
|
-
/** Most recent URL in the record file, or null. */
|
|
19
|
-
lastUrl(): string | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** First existing executable named `name` on `env.PATH`, or null. */
|
|
23
|
-
export function findOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
24
|
-
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
25
|
-
if (!dir) continue;
|
|
26
|
-
const candidate = path.join(dir, name);
|
|
27
|
-
try {
|
|
28
|
-
const st = fs.statSync(candidate);
|
|
29
|
-
if (st.isFile() && (st.mode & 0o111) !== 0) return candidate;
|
|
30
|
-
} catch {
|
|
31
|
-
/* not here */
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Last http(s) line of `file`, or null (absent/unreadable counts as empty). */
|
|
38
|
-
export function readLastUrl(file: string): string | null {
|
|
39
|
-
try {
|
|
40
|
-
const lines = fs.readFileSync(file, "utf8").split("\n").filter((l) => /^https?:\/\//.test(l));
|
|
41
|
-
return lines.at(-1) ?? null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Port of the OAuth loopback redirect (redirect_uri param), or null. */
|
|
48
|
-
export function parseAuthPort(url: string): number | null {
|
|
49
|
-
try {
|
|
50
|
-
const redirect = new URL(url).searchParams.get("redirect_uri");
|
|
51
|
-
if (!redirect) return null;
|
|
52
|
-
const port = new URL(redirect).port;
|
|
53
|
-
return port ? Number(port) : null;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const shQuote = (s: string): string => `'${s.replace(/'/g, "'\\''")}'`;
|
|
60
|
-
|
|
61
|
-
/** Create the capture (record file + wrapper script) and return the spawn
|
|
62
|
-
* env. Idempotent: the wrapper is rewritten only when content changes. The
|
|
63
|
-
* opener to forward to is resolved once, at write time: an existing BROWSER
|
|
64
|
-
* wins, then xdg-open, then macOS open; with none available the wrapper
|
|
65
|
-
* records the URL and exits (headless: capture without an opener). The
|
|
66
|
-
* forward word-splits the BROWSER value, so flag forms (`open -a Chrome`)
|
|
67
|
-
* work; a BROWSER path containing spaces does not (rare, and BROWSER lists
|
|
68
|
-
* are not honored either - the first entry is what chains). Windows browser
|
|
69
|
-
* resolution differs; no capture there. */
|
|
70
|
-
export function setupAuthUrlCapture(dataDir = path.join(os.homedir(), ".pi", "agent", "antigravity-bridge"), env: NodeJS.ProcessEnv = process.env): AuthUrlCapture | null {
|
|
71
|
-
if (process.platform === "win32") return null;
|
|
72
|
-
const file = path.join(dataDir, "acp-auth-urls.log");
|
|
73
|
-
const wrapper = path.join(dataDir, "acp-browser-wrapper.sh");
|
|
74
|
-
try {
|
|
75
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
76
|
-
// Truncate: the file mirrors the current login attempt only. OAuth
|
|
77
|
-
// URLs carry state + PKCE challenges, so keep the record private.
|
|
78
|
-
fs.writeFileSync(file, "", { mode: 0o600 });
|
|
79
|
-
const opener = env.BROWSER?.trim() || findOnPath("xdg-open", env) || findOnPath("open", env) || "";
|
|
80
|
-
const body = [
|
|
81
|
-
"#!/usr/bin/env sh",
|
|
82
|
-
"# Antigravity bridge: records the Google OAuth URL the ACP server hands",
|
|
83
|
-
"# to the browser, then forwards to the real opener. The bridge watches",
|
|
84
|
-
"# the record file and shows the URL in pi (copy it when logging in over",
|
|
85
|
-
"# SSH; forward the redirect port first).",
|
|
86
|
-
`printf '%s\\n' "$@" >> ${shQuote(file)}`,
|
|
87
|
-
`REAL=${shQuote(opener)}`,
|
|
88
|
-
'if [ -n "$REAL" ]; then set -f; exec $REAL "$@"; fi',
|
|
89
|
-
"",
|
|
90
|
-
].join("\n");
|
|
91
|
-
const write = (p: string, content: string, mode: number): void => {
|
|
92
|
-
try {
|
|
93
|
-
if (fs.readFileSync(p, "utf8") === content) return;
|
|
94
|
-
} catch {
|
|
95
|
-
/* new file */
|
|
96
|
-
}
|
|
97
|
-
fs.writeFileSync(p, content, { mode });
|
|
98
|
-
};
|
|
99
|
-
write(wrapper, body, 0o755);
|
|
100
|
-
return { browserEnv: { BROWSER: wrapper }, file, lastUrl: () => readLastUrl(file) };
|
|
101
|
-
} catch {
|
|
102
|
-
return null; // capture is best effort; login still works without it
|
|
103
|
-
}
|
|
104
|
-
}
|