@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/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
- }