@estebanforge/pi-antigravity-bridge 1.4.4 → 1.4.6
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 +27 -0
- package/README.md +5 -4
- package/docs/ACP-ADOPTION-PLAN.md +2 -2
- package/docs/DEVELOPMENT.md +1 -1
- package/extensions/index.ts +85 -29
- package/package.json +1 -1
- package/src/acp/auth.ts +81 -0
- package/src/acp/connection.ts +2 -2
- package/src/acp/setup.ts +11 -6
- package/src/provider.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.4.6] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Tool-priority note in the system prompt block (`systemPrompt` setting, on by default): agy runs embedded in pi, so its native interactive tools (the live example was `ask_question` vs the bridge's `ask_user_question`) never reach the user. The note tells agy to always prefer the Pi Bridge tool when one covers the same purpose. The old preamble sentence that allowed "your own tools or the pi tool bridge" is gone, since that ambiguity was the cause.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `/agy auth` reports a distinct failure instead of a false "Signed in." when the server accepts the sign-in but `acp_token.json` never appears within the 5 s post-authenticate grace window (peer-review find; the poll result was never read).
|
|
14
|
+
- Test-infra: the fake ACP server's request log was lost on SIGTERM (buffered write stream + Gate D teardown), which flaked the Gate D abort test ~1-in-3 suite runs; the log now appends synchronously.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The sign-in URL toast fences the URL with blank lines so it stays readable and copyable next to the ssh port-forward hint.
|
|
19
|
+
|
|
20
|
+
## [1.4.5] - 2026-09-04
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Tool-priority note in the system prompt block (`systemPrompt` setting, on by default): agy runs embedded in pi, so its native interactive tools (the live example was `ask_question` vs the bridge's `ask_user_question`) never reach the user. The note tells agy to always prefer the Pi Bridge tool when one covers the same purpose. The old preamble sentence that allowed "your own tools or the pi tool bridge" is gone, since that ambiguity was the cause.
|
|
25
|
+
|
|
26
|
+
- `/agy auth` subcommand: runs the antigravity-acp sign-in on demand (spawns the server, sends `authenticate`, waits for the browser round-trip to complete; minutes-scale). Signing in no longer rides the first Antigravity message, and both login-pending moments now point at `/agy auth`. The sign-in URL toast (and its ssh port-forward hint) fires during `/agy auth` exactly as it does during turns, and a second concurrent `/agy auth` fails fast instead of racing the token write.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `/agy acp-auth` renamed to `/agy auth-manual` (no alias): it prints the manual credential setup for those who want it. Auth-error remediation, MANUAL_SETUP, README, and docs now point at `/agy auth` / `/agy auth-manual`, and the stale first-message login wording is gone from the manual steps.
|
|
31
|
+
|
|
5
32
|
## [1.4.4] - 2026-09-04
|
|
6
33
|
|
|
7
34
|
### Added
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Turns run through one of two engines behind the same provider surface (`config.e
|
|
|
25
25
|
|
|
26
26
|
Engine-dependent features: pi image attachments ride natively only on the ACP engine (the picker offers image attach automatically when `config.engine` is `acp`; the stream-json CLI prompt is text-only). With the optional G1 digest enabled, its delivery also differs: ACP ships it as a native `embeddedContext` resource block, stream-json prepends it to the prompt text.
|
|
27
27
|
|
|
28
|
-
Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription:
|
|
28
|
+
Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: the same account and plan you use for the Antigravity CLI (`agy`). Sign in explicitly with `/agy auth` (engine `acp` selected): it opens the Google login in your browser and completes when you finish it. If no browser is available (an SSH session on a remote machine), pi shows the sign-in URL to copy, plus the ssh port-forward command for the login redirect. It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy auth-manual`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
|
|
29
29
|
|
|
30
30
|
## What it cannot do
|
|
31
31
|
|
|
@@ -107,7 +107,7 @@ If `agy models` fails at load (binary missing, auth not done, network stall), a
|
|
|
107
107
|
| `askTool` | `on` (register the AskAntigravity delegation tool), `off` (no delegation tool; provider and models only) | `on` |
|
|
108
108
|
| `bridgeTools` | `none` (bridge off), `all` (every non-builtin tool, incl. other `Ask*` delegations), `mcp` (pi-mcp-adapter tools + skills bridge only) | `all` |
|
|
109
109
|
| `digest` | `off` (stable prompts; agy's prompt cache hits) or `on` (inject a delta of pi-side context - compaction summaries, other-provider turns - into each agy prompt; the delta changes every turn, so agy re-bills the full context). Enable for mixed-provider sessions where agy must see pi-side context | `off` |
|
|
110
|
-
| `systemPrompt` | `on` (prepend pi's system prompt - operating instructions plus the global agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` - to the first prompt of each new agy conversation) or `off` (agy-native behavior) | `on` |
|
|
110
|
+
| `systemPrompt` | `on` (prepend pi's system prompt - operating instructions plus the global agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` - to the first prompt of each new agy conversation, plus a tool-priority note: Pi Bridge tools win over agy's native interactive ones, which never reach the user) or `off` (agy-native behavior) | `on` |
|
|
111
111
|
|
|
112
112
|
Env overrides: `AGY_BRIDGE_TOOLS`, `AGY_ASK_TOOL`, `AGY_DIGEST`, `AGY_SYSTEM_PROMPT`. Env wins over the file, so while `AGY_DIGEST` or `AGY_SYSTEM_PROMPT` is set, the matching `/agy digest` or `/agy system-prompt` toggle persists a value that never takes effect.
|
|
113
113
|
|
|
@@ -121,6 +121,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
121
121
|
/agy status, or open the full settings picker (TUI)
|
|
122
122
|
/agy status print current settings + session counts
|
|
123
123
|
/agy doctor bridge state, driver counters, bridge port, last lifecycle events
|
|
124
|
+
/agy auth run the antigravity-acp sign-in now (engine acp): opens the Google login in your browser, shows the URL when no browser opens
|
|
124
125
|
/agy mode plan review-only: agy plans but writes nothing
|
|
125
126
|
/agy mode accept-edits agy applies edits directly (default)
|
|
126
127
|
/agy permissions on|off auto-approve / prompt for tool calls (see warning)
|
|
@@ -128,11 +129,11 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
128
129
|
/agy model flash|pro|gemini fallback model for the AskAntigravity tool; callers may override per call
|
|
129
130
|
/agy thinking low|medium|high fallback thinking tier for the AskAntigravity tool; callers may override per call
|
|
130
131
|
/agy digest on|off inject pi-side context into agy prompts (default off; see table above)
|
|
131
|
-
/agy system-prompt on|off send pi's system prompt + AGENTS.md to new agy conversations (default on)
|
|
132
|
+
/agy system-prompt on|off send pi's system prompt + AGENTS.md + the Pi Bridge tool-priority note to new agy conversations (default on)
|
|
132
133
|
/agy bridge all|mcp|none which pi tools the MCP bridge exposes to agy (default all; none = bridge off)
|
|
133
134
|
/agy acp-bin <path|auto> point the ACP engine at a specific server binary (auto = setup installs, or AGY_ACP_BIN; applies on the next ACP turn)
|
|
134
135
|
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp runs self-service setup: binary install + auth bootstrap)
|
|
135
|
-
/agy
|
|
136
|
+
/agy auth-manual manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
|
|
136
137
|
/agy patch-cleanup restore the original pi files if an older version patched them
|
|
137
138
|
/agy clear drop all session bindings (force fresh conversations)
|
|
138
139
|
```
|
|
@@ -325,7 +325,7 @@ Tested with the parity suite (section 11) and `scripts/parity-live.mjs`
|
|
|
325
325
|
| Human decision latency on parked G9 tool round-trips exceeds the 10m overall-turn cap | Turn killed mid-wait | `AcpDriver` pauses the overall timer while parked, re-armed with REMAINING budget; every park carries its own timeout (9.3) |
|
|
326
326
|
| Binary size (1.5 GB + 117 MB) | Disk + update churn | Pinned install layout (section 12), registry pin by build id |
|
|
327
327
|
| aarch64 TCMalloc startup failure (forum report, Chromebook) | arch-specific | x86_64 unaffected; gate install per-arch, link the forum issue in README |
|
|
328
|
-
| Separate auth state from `agy` CLI | First-run auth friction | `/agy
|
|
328
|
+
| Separate auth state from `agy` CLI | First-run auth friction | `/agy auth-manual` prints exact instructions; `gemini-api-key` path works headless via `GEMINI_API_KEY`. The agent never writes credentials |
|
|
329
329
|
| Thought text changes rendering volume | Noisy transcript | Route through the existing thinking block pipeline, close-on-switch unchanged; no new block types |
|
|
330
330
|
| Two engines drift on behavior | Confusing support surface | Shared provider layer + shared parity suite run against both engines in CI |
|
|
331
331
|
|
|
@@ -827,7 +827,7 @@ Install layout:
|
|
|
827
827
|
request (issue) once we depend on this.
|
|
828
828
|
- `--uid=` from the linux registry entry: the server ran without it here.
|
|
829
829
|
Treat as a launcher artifact; revisit if a future build refuses to start.
|
|
830
|
-
- Auth onboarding: `/agy
|
|
830
|
+
- Auth onboarding: `/agy auth-manual` prints the four methods with exact
|
|
831
831
|
`settings.json` shapes (source: official Zed docs, section 2.3). Headless
|
|
832
832
|
path: `GEMINI_API_KEY` + `auth.type: "gemini-api-key"`. The agent NEVER
|
|
833
833
|
writes or reads credentials; it prints instructions and verifies by
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -37,7 +37,7 @@ AGY_LIVE=1 node --experimental-strip-types scripts/smoke-stream-json.mjs
|
|
|
37
37
|
|
|
38
38
|
# Live smoke for the ACP engine through OUR driver stack. OPT-IN: spends a
|
|
39
39
|
# little quota. Needs AGY_ACP_BIN (or acp on PATH) and a one-time
|
|
40
|
-
# /agy
|
|
40
|
+
# /agy auth-manual credential setup.
|
|
41
41
|
AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par \
|
|
42
42
|
npx tsx scripts/smoke-acp.mjs
|
|
43
43
|
|
package/extensions/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { SessionStore } from "../src/sessions.js";
|
|
|
40
40
|
import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
|
|
41
41
|
import { AgyDriver } from "../src/driver.js";
|
|
42
42
|
import { AcpDriver } from "../src/acp/driver.js";
|
|
43
|
+
import { runAcpAuth } from "../src/acp/auth.js";
|
|
43
44
|
import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
|
|
44
45
|
import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
45
46
|
import type { TurnDriver } from "../src/driver-types.js";
|
|
@@ -135,24 +136,36 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
135
136
|
"unsupported-server-request",
|
|
136
137
|
]);
|
|
137
138
|
const legacyDriver = new AgyDriver();
|
|
139
|
+
// Shared ACP log sink (driver turns AND /agy auth): the login URL event
|
|
140
|
+
// toasts so SSH users can copy it; genuine failures reach stderr.
|
|
141
|
+
const acpLog = (msg: string, data?: unknown): void => {
|
|
142
|
+
if (msg === "auth-url") {
|
|
143
|
+
const { url, port } = (data ?? {}) as { url?: string; port?: number | null };
|
|
144
|
+
if (!url) return;
|
|
145
|
+
// Blank lines fence the URL off from the rest of the text: it is long
|
|
146
|
+
// and wraps, so separation keeps it readable and copyable.
|
|
147
|
+
const parts = ["Google sign-in URL for the ACP engine:", "", url];
|
|
148
|
+
if (port) {
|
|
149
|
+
parts.push(
|
|
150
|
+
"",
|
|
151
|
+
"SSH session? Forward the port on your machine first:",
|
|
152
|
+
` ssh -N -L ${port}:127.0.0.1:${port} <user@host>`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const text = parts.join("\n");
|
|
156
|
+
if (activeUi) activeUi.notify(text, "warning");
|
|
157
|
+
else console.error(`[antigravity-bridge acp] ${text}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!acpFailures.has(msg)) return;
|
|
161
|
+
console.error(`[antigravity-bridge acp] ${msg}${data !== undefined ? " " + JSON.stringify(data) : ""}`);
|
|
162
|
+
};
|
|
138
163
|
const acpDriver = new AcpDriver({
|
|
139
164
|
// Resolved per connection: the setup flow can install the binary and
|
|
140
165
|
// update acp.bin mid-session; the next turn picks it up (no restart).
|
|
141
166
|
bin: () => loadConfig().acp.bin,
|
|
142
167
|
...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
|
|
143
|
-
log:
|
|
144
|
-
if (msg === "auth-url") {
|
|
145
|
-
const { url, port } = (data ?? {}) as { url?: string; port?: number | null };
|
|
146
|
-
if (!url) return;
|
|
147
|
-
const ssh = port ? `\nSSH session? Forward the port on your machine first:\n ssh -N -L ${port}:127.0.0.1:${port} <user@host>` : "";
|
|
148
|
-
const text = `Google sign-in URL for the ACP engine:\n${url}${ssh}`;
|
|
149
|
-
if (activeUi) activeUi.notify(text, "warning");
|
|
150
|
-
else console.error(`[antigravity-bridge acp] ${text}`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (!acpFailures.has(msg)) return;
|
|
154
|
-
console.error(`[antigravity-bridge acp] ${msg}${data !== undefined ? " " + JSON.stringify(data) : ""}`);
|
|
155
|
-
},
|
|
168
|
+
log: acpLog,
|
|
156
169
|
mcpServers: () => {
|
|
157
170
|
const handle = mcpHandle;
|
|
158
171
|
if (!handle) return [];
|
|
@@ -232,6 +245,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
232
245
|
acpDriver,
|
|
233
246
|
engine,
|
|
234
247
|
getMcpPort: () => mcpHandle?.port ?? null,
|
|
248
|
+
acpLog,
|
|
249
|
+
authCapture: authCapture ?? null,
|
|
235
250
|
});
|
|
236
251
|
|
|
237
252
|
// AskAntigravity tool: one-shot delegation to agy (ported from
|
|
@@ -299,7 +314,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
299
314
|
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
300
315
|
}
|
|
301
316
|
if (status.needsLogin) {
|
|
302
|
-
const msg = acpLoginPending(
|
|
317
|
+
const msg = acpLoginPending();
|
|
303
318
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
304
319
|
else console.error(`[antigravity-bridge] ${msg}`);
|
|
305
320
|
}
|
|
@@ -416,11 +431,11 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
416
431
|
// --- /agy command -----------------------------------------------------------
|
|
417
432
|
|
|
418
433
|
/** Shared body for every ACP-login-pending moment (session_start self-heal,
|
|
419
|
-
* /agy engine acp
|
|
420
|
-
* account.
|
|
421
|
-
*
|
|
422
|
-
function acpLoginPending(
|
|
423
|
-
return `One-time sign-in needed to finish ACP setup.
|
|
434
|
+
* /agy engine acp). End-user simple: what to run, what happens, which
|
|
435
|
+
* account. Sign-in is explicit (/agy auth); it never rides the first
|
|
436
|
+
* message. */
|
|
437
|
+
function acpLoginPending(): string {
|
|
438
|
+
return `One-time sign-in needed to finish ACP setup. Run /agy auth: the Google sign-in opens in your browser. Use the Google account of your Antigravity subscription (the same account as your agy CLI login). If no browser opens, pi shows the sign-in URL to copy. The token stays on your machine; this extension never sees it.`;
|
|
424
439
|
}
|
|
425
440
|
|
|
426
441
|
interface AgyCommandCtx {
|
|
@@ -432,6 +447,11 @@ interface AgyCommandCtx {
|
|
|
432
447
|
/** Engine latched at extension load (see the provider wiring note). */
|
|
433
448
|
engine: Engine;
|
|
434
449
|
getMcpPort: () => number | null;
|
|
450
|
+
/** Shared ACP log sink (login URL surfacing + failure events). */
|
|
451
|
+
acpLog: (msg: string, data?: unknown) => void;
|
|
452
|
+
/** BROWSER-capture handles; null when unavailable (Windows, unwritable
|
|
453
|
+
* data dir). /agy auth passes them to the sign-in process. */
|
|
454
|
+
authCapture: { browserEnv: Record<string, string>; file: string } | null;
|
|
435
455
|
}
|
|
436
456
|
|
|
437
457
|
interface PendingConfig {
|
|
@@ -467,7 +487,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
467
487
|
row("digest:", config.digest ? "on" : "off"),
|
|
468
488
|
row("system prompt:", config.systemPrompt ? "on" : "off"),
|
|
469
489
|
"",
|
|
470
|
-
"Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy bridge all|mcp|none, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy
|
|
490
|
+
"Subcommands: /agy auth, /agy auth-manual, /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy bridge all|mcp|none, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear",
|
|
471
491
|
].join("\n");
|
|
472
492
|
}
|
|
473
493
|
|
|
@@ -475,7 +495,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
475
495
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
476
496
|
pi.registerCommand("agy", {
|
|
477
497
|
description:
|
|
478
|
-
"Antigravity provider: status, doctor, settings picker, clear sessions. Usage: /agy [status|doctor|engine stream-json|acp|mode plan|accept-edits|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|bridge all|mcp|none|digest on|off|system-prompt on|off|acp-bin <path|auto>|
|
|
498
|
+
"Antigravity provider: status, doctor, settings picker, clear sessions. Usage: /agy [status|doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|bridge all|mcp|none|digest on|off|system-prompt on|off|acp-bin <path|auto>|patch-cleanup|clear]",
|
|
479
499
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
480
500
|
const ui = cmdCtx.ui;
|
|
481
501
|
if (ui) activeUi = ui;
|
|
@@ -535,7 +555,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
535
555
|
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
536
556
|
if (status.needsLogin) {
|
|
537
557
|
ui?.notify(
|
|
538
|
-
`ACP engine set. ${acpLoginPending(
|
|
558
|
+
`ACP engine set. ${acpLoginPending()}`,
|
|
539
559
|
"warning",
|
|
540
560
|
);
|
|
541
561
|
} else {
|
|
@@ -546,6 +566,42 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
546
566
|
}
|
|
547
567
|
return;
|
|
548
568
|
}
|
|
569
|
+
if (sub === "auth") {
|
|
570
|
+
// Gate on the CONFIGURED engine (disk + env), not the latched one:
|
|
571
|
+
// /agy engine acp followed by /agy auth in the same session works,
|
|
572
|
+
// no restart needed before signing in.
|
|
573
|
+
if (loadConfig().engine !== "acp") {
|
|
574
|
+
ui?.notify(`the selected engine is ${loadConfig().engine}. /agy engine acp first, then /agy auth.`, "warning");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
ui?.notify("Preparing the ACP server (binary + auth settings)…", "info");
|
|
578
|
+
const status = await ensureAcpReady({ configBin: loadConfig().acp.bin, onProgress: (m) => ui?.notify(m, "info") });
|
|
579
|
+
if (!status.ok) {
|
|
580
|
+
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
584
|
+
if (!status.needsLogin) {
|
|
585
|
+
ui?.notify(`Already signed in (auth: ${status.auth}). Nothing to do.`, "info");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
ui?.notify(
|
|
589
|
+
"Signing in: the Google sign-in opens in your browser and completes when you finish it (minutes-scale). If no browser opens, pi shows the sign-in URL to copy.",
|
|
590
|
+
"info",
|
|
591
|
+
);
|
|
592
|
+
const r = await runAcpAuth({
|
|
593
|
+
bin: status.bin,
|
|
594
|
+
extraEnv: ctx.authCapture?.browserEnv,
|
|
595
|
+
authUrlFile: ctx.authCapture?.file,
|
|
596
|
+
log: ctx.acpLog,
|
|
597
|
+
});
|
|
598
|
+
if (r.ok) {
|
|
599
|
+
ui?.notify("Signed in. The ACP engine is ready; takes effect on the next pi start (or /reload).", "info");
|
|
600
|
+
} else {
|
|
601
|
+
ui?.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
549
605
|
if (sub === "acp-bin") {
|
|
550
606
|
const rest = (args ?? "").trim().split(/\s+/).slice(1).join(" ");
|
|
551
607
|
if (rest.length > 0) {
|
|
@@ -564,7 +620,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
564
620
|
}
|
|
565
621
|
return;
|
|
566
622
|
}
|
|
567
|
-
if (sub === "
|
|
623
|
+
if (sub === "auth-manual") {
|
|
568
624
|
ui?.notify(
|
|
569
625
|
[
|
|
570
626
|
"ACP engine authentication (one-time; usually automatic -",
|
|
@@ -580,16 +636,16 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
580
636
|
"1. Server binary: auto-setup installs it. Manual: agy_acp_server.par from",
|
|
581
637
|
" the antigravity-acp registry; point acp.bin or AGY_ACP_BIN at it.",
|
|
582
638
|
'2. Default: put {"auth":{"type":"oauth-personal"}} in',
|
|
583
|
-
" ~/.gemini/antigravity-acp/settings.json, run
|
|
584
|
-
" Google login that opens in your browser. No browser (SSH
|
|
585
|
-
" pi shows the sign-in URL to copy; forward the redirect
|
|
586
|
-
" (ssh -N -L <port>:127.0.0.1:<port> <user@host>), then
|
|
587
|
-
" on your machine.",
|
|
639
|
+
" ~/.gemini/antigravity-acp/settings.json, run /agy auth, and complete",
|
|
640
|
+
" the Google login that opens in your browser. No browser (SSH",
|
|
641
|
+
" session)? pi shows the sign-in URL to copy; forward the redirect",
|
|
642
|
+
" port over ssh (ssh -N -L <port>:127.0.0.1:<port> <user@host>), then",
|
|
643
|
+
" open the URL on your machine.",
|
|
588
644
|
' Headless alternative: GEMINI_API_KEY + {"auth":{"type":"gemini-api-key"}}',
|
|
589
645
|
" (metered paid API - not your Antigravity plan). The key is used only",
|
|
590
646
|
" when that type is selected; with the default oauth-personal in place,",
|
|
591
647
|
" an exported key is ignored.",
|
|
592
|
-
"3.
|
|
648
|
+
"3. /agy doctor shows the server version when auth is OK.",
|
|
593
649
|
].join("\n"),
|
|
594
650
|
"info",
|
|
595
651
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/acp/auth.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AcpConnection } from "./connection.js";
|
|
2
|
+
import { hasAcpToken } 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
|
+
export const AUTH_TIMEOUT_MS = 10 * 60_000;
|
|
14
|
+
|
|
15
|
+
export interface AcpAuthOptions {
|
|
16
|
+
bin: string;
|
|
17
|
+
binArgs?: string[];
|
|
18
|
+
cwd?: string;
|
|
19
|
+
extraEnv?: Record<string, string>;
|
|
20
|
+
authUrlFile?: string;
|
|
21
|
+
/** Directory holding acp_token.json. Default: ~/.gemini/antigravity-acp.
|
|
22
|
+
* Tests point this at a fixture dir; production callers use the default. */
|
|
23
|
+
tokenDir?: string;
|
|
24
|
+
/** The server is expected to have written acp_token.json by the time
|
|
25
|
+
* authenticate replies, but write-after-reply is unverified; after a
|
|
26
|
+
* successful reply the run polls for the token up to this long before
|
|
27
|
+
* SIGTERM, so a laggard write is never killed mid-flight. A token that
|
|
28
|
+
* never appears is a distinct failure, not silent success. */
|
|
29
|
+
tokenGraceMs?: number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
log?: (msg: string, data?: unknown) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AcpAuthResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
/** Failure reason (spawn, timeout, -32000 family, concurrent run). Absent when ok. */
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let authInFlight: Promise<AcpAuthResult> | null = null;
|
|
41
|
+
|
|
42
|
+
export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
43
|
+
const conn = new AcpConnection({
|
|
44
|
+
bin: opts.bin,
|
|
45
|
+
binArgs: opts.binArgs,
|
|
46
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
47
|
+
extraEnv: opts.extraEnv,
|
|
48
|
+
authUrlFile: opts.authUrlFile,
|
|
49
|
+
log: opts.log ?? (() => {}),
|
|
50
|
+
onUpdate: () => {},
|
|
51
|
+
onExit: () => {},
|
|
52
|
+
});
|
|
53
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
54
|
+
const grace = opts.tokenGraceMs ?? 5_000;
|
|
55
|
+
const run = async (): Promise<AcpAuthResult> => {
|
|
56
|
+
try {
|
|
57
|
+
await conn.start();
|
|
58
|
+
await conn.request("authenticate", { methodId: "oauth-personal" }, opts.timeoutMs ?? AUTH_TIMEOUT_MS);
|
|
59
|
+
const deadline = Date.now() + grace;
|
|
60
|
+
while (!hasAcpToken(opts.tokenDir) && Date.now() < deadline) await sleep(200);
|
|
61
|
+
if (!hasAcpToken(opts.tokenDir)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: `the server accepted the sign-in, but no token file appeared in ${opts.tokenDir ?? "~/.gemini/antigravity-acp"} within ${Math.round(grace / 1000)}s`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { ok: true };
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
70
|
+
} finally {
|
|
71
|
+
conn.kill();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// One sign-in at a time: two concurrent runs would race the OAuth loopback
|
|
75
|
+
// and the token write. A second invocation fails fast instead.
|
|
76
|
+
if (authInFlight) return Promise.resolve({ ok: false, error: "a sign-in is already running; wait for it to finish" });
|
|
77
|
+
authInFlight = run();
|
|
78
|
+
return authInFlight.finally(() => {
|
|
79
|
+
authInFlight = null;
|
|
80
|
+
});
|
|
81
|
+
}
|
package/src/acp/connection.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface AcpSessionInfo {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Auth failed (-32000 family). Message carries the remediation the server
|
|
41
|
-
* provided plus the /agy
|
|
41
|
+
* provided plus the /agy auth-manual pointer. */
|
|
42
42
|
export class AcpAuthError extends Error {
|
|
43
43
|
constructor(message: string) {
|
|
44
44
|
super(message);
|
|
@@ -424,7 +424,7 @@ export function translateError(method: string, err: unknown): Error {
|
|
|
424
424
|
if (err.code === -32000) {
|
|
425
425
|
const detail = typeof err.data === "object" && err.data !== null ? (err.data as { message?: unknown }).message : undefined;
|
|
426
426
|
const detailText = typeof detail === "string" ? `: ${detail}` : `: ${err.message}`;
|
|
427
|
-
return new AcpAuthError(`ACP authentication required for ${method}${detailText}. Run /agy
|
|
427
|
+
return new AcpAuthError(`ACP authentication required for ${method}${detailText}. Run /agy auth to sign in; /agy auth-manual has manual steps.`);
|
|
428
428
|
}
|
|
429
429
|
// Typed protocol errors survive: callers probe .code (-32601 cancel-
|
|
430
430
|
// unsupported, -32602 param shape). The method prefix is lost — describe()
|
package/src/acp/setup.ts
CHANGED
|
@@ -48,8 +48,8 @@ export type AcpSetupStatus =
|
|
|
48
48
|
bin: string;
|
|
49
49
|
binarySource: "env" | "config" | "installed" | "existing";
|
|
50
50
|
auth: string;
|
|
51
|
-
/** True when auth was just set to oauth-personal:
|
|
52
|
-
* Google login in the browser
|
|
51
|
+
/** True when auth was just set to oauth-personal: /agy auth opens
|
|
52
|
+
* the Google login in the browser. */
|
|
53
53
|
needsLogin: boolean;
|
|
54
54
|
/** Human-readable actions taken (empty when everything was ready). */
|
|
55
55
|
actions: string[];
|
|
@@ -70,10 +70,10 @@ export const MANUAL_SETUP = [
|
|
|
70
70
|
" (~/.local/opt/agy-acp/current/agy_acp_server.par works; 'current' is a",
|
|
71
71
|
" symlink to the build dir).",
|
|
72
72
|
'3. Log in: put {"auth":{"type":"oauth-personal"}} in',
|
|
73
|
-
" ~/.gemini/antigravity-acp/settings.json and
|
|
74
|
-
" opens in your browser
|
|
73
|
+
" ~/.gemini/antigravity-acp/settings.json and run /agy auth, then complete",
|
|
74
|
+
" the Google login that opens in your browser. That login IS your",
|
|
75
75
|
" Antigravity subscription (Google AI Plus/Pro/Ultra).",
|
|
76
|
-
"Details: /agy
|
|
76
|
+
"Details: /agy auth-manual",
|
|
77
77
|
].join("\n");
|
|
78
78
|
|
|
79
79
|
export function platformKey(): string {
|
|
@@ -239,6 +239,11 @@ function tokenPresent(target: string): boolean {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/** Whether a login token exists (default dir, or an explicit one). */
|
|
243
|
+
export function hasAcpToken(dir?: string): boolean {
|
|
244
|
+
return tokenPresent(dir ?? path.join(os.homedir(), ".gemini", "antigravity-acp"));
|
|
245
|
+
}
|
|
246
|
+
|
|
242
247
|
/** Auth state WITHOUT reading any credential value: settings.json carries
|
|
243
248
|
* only auth.type (+ non-secret gcp placement), acp_token.json is stat()ed. */
|
|
244
249
|
export function readAuthState(dir?: string): AuthState {
|
|
@@ -334,7 +339,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
334
339
|
// before the first spawn, so a fresh write takes effect. oauth-personal is
|
|
335
340
|
// THE default: it is the Antigravity subscription (same Google account and
|
|
336
341
|
// plan as the agy CLI login). gemini-api-key (metered paid API) stays a
|
|
337
|
-
// manual option for headless boxes (/agy
|
|
342
|
+
// manual option for headless boxes (/agy auth-manual); never the default.
|
|
338
343
|
const gdir = defaultGeminiDir(opts);
|
|
339
344
|
const auth = readAuthState(gdir);
|
|
340
345
|
if (!auth.configured) {
|
package/src/provider.ts
CHANGED
|
@@ -108,10 +108,19 @@ const DIGEST_PREAMBLE =
|
|
|
108
108
|
// afterwards, and agy's server-side prompt cache keeps hitting.
|
|
109
109
|
|
|
110
110
|
export const SYSTEM_PROMPT_PREAMBLE =
|
|
111
|
-
"[The following is the system prompt of the pi session that spawned this conversation: operating instructions plus project context (AGENTS.md files). Apply it for this whole conversation.
|
|
111
|
+
"[The following is the system prompt of the pi session that spawned this conversation: operating instructions plus project context (AGENTS.md files). Apply it for this whole conversation.]";
|
|
112
112
|
|
|
113
113
|
export const SYSTEM_PROMPT_END = "[END SYSTEM PROMPT]";
|
|
114
114
|
|
|
115
|
+
/** Brief tool-priority note appended inside every system prompt block: agy
|
|
116
|
+
* runs embedded in pi, so its native interactive tools never reach the
|
|
117
|
+
* user. Equivalent Pi Bridge tools must win. Concrete clash observed live:
|
|
118
|
+
* agy picked its native ask_question over the bridge's ask_user_question
|
|
119
|
+
* and the question never displayed. Rides the systemPrompt gate: the note
|
|
120
|
+
* ships only when the system prompt ships. */
|
|
121
|
+
export const TOOL_PRIORITY_NOTE =
|
|
122
|
+
"[Tool priority: this conversation runs inside pi, not as a standalone agy session; the user only sees what surfaces in pi. Native interactive tools, for example ask_question, never reach the user. When a Pi Bridge tool covers the same purpose, always use the Pi Bridge tool; for user questions use ask_user_question.]";
|
|
123
|
+
|
|
115
124
|
/** Assemble the full agy prompt: system prompt block, pi-side digest, user
|
|
116
125
|
* prompt. Empty parts are dropped. Pure; exported for unit testing.
|
|
117
126
|
* Pass systemPrompt only on a fresh conversation (see runTurnDriver). */
|
|
@@ -122,7 +131,7 @@ export function buildFullPrompt(
|
|
|
122
131
|
): string {
|
|
123
132
|
const parts: string[] = [];
|
|
124
133
|
if (systemPrompt) {
|
|
125
|
-
parts.push(`${SYSTEM_PROMPT_PREAMBLE}\n\n${systemPrompt}\n\n${SYSTEM_PROMPT_END}`);
|
|
134
|
+
parts.push(`${SYSTEM_PROMPT_PREAMBLE}\n\n${systemPrompt}\n\n${TOOL_PRIORITY_NOTE}\n\n${SYSTEM_PROMPT_END}`);
|
|
126
135
|
}
|
|
127
136
|
if (digest) {
|
|
128
137
|
parts.push(`${DIGEST_PREAMBLE}\n\n${digest}`);
|