@estebanforge/pi-antigravity-bridge 1.9.2 → 1.9.3
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 +10 -0
- package/extensions/index.ts +7 -1
- package/package.json +1 -1
- package/src/acp/auth.ts +66 -11
- package/src/acp/connection.ts +9 -4
- package/src/acp/driver.ts +16 -6
- package/src/acp/setup.ts +50 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.9.3] - 2026-11-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A silently dying ACP server now reports its exit code and signal.** The exit handler received the child's code and signal but dropped them (hardcoded null), so a `.par` that died before answering initialize surfaced only as "process gone" with no cause (issue #9). The code and signal now reach the abort reason and the user-facing mid-turn failure, and the `connection-exited` log line records them alongside the stderr tail.
|
|
10
|
+
|
|
11
|
+
- **The registry's declared `distribution.binary.args` are now honored.** The installer read only `archive` and `cmd`, so a Linux-only arg like `--uid=` was silently dropped and the server never received it (issue #9). Args are validated as a flat string array, persisted to a sidecar `args.json` next to `zip.sha256` (so the already-installed early-return path recovers them), and resolved by path identity against the managed `<root>/current` binary: setup persists that path into `acp.bin`, so a source-label check would wrongly treat it as a user override on the next turn. Env and config overrides (a user shim such as `agy acp`) own their argv and get no registry args. Both `/agy auth` flows and the per-connection driver receive them.
|
|
12
|
+
|
|
13
|
+
- **A ToS-disabled Google account is detected at sign-in instead of on the first real prompt.** The credential probe used only `session/new`, which a disabled account passes, so it wrote a sticky verified marker and `/agy doctor` kept reporting healthy (issue #9). The probe now sends one bounded `session/prompt` and scans the streamed reply for the disablement notice (Google returns it as normal content, not a -32000 auth error). A disabled account fails auth immediately with appeal guidance and writes no marker; the probe is time-capped so a hollow or hung server fails rather than hanging `/agy auth`, and the reply head is logged so a change in Google's wording is diagnosable. The probe is scoped to the token-absent branch to avoid a model call on every normal browser sign-in.
|
|
14
|
+
|
|
5
15
|
## [1.9.2] - 2026-11-02
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/extensions/index.ts
CHANGED
|
@@ -62,7 +62,7 @@ import { AcpDriver } from "../src/acp/driver.js";
|
|
|
62
62
|
import { runAcpAuth } from "../src/acp/auth.js";
|
|
63
63
|
import { setAuthInvalidatedHook } from "../src/acp/connection.js";
|
|
64
64
|
import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
|
|
65
|
-
import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
65
|
+
import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup, resolveInstalledArgs } from "../src/acp/setup.js";
|
|
66
66
|
import { ensureQueuedMessagesDefaults } from "../src/queued-messages.js";
|
|
67
67
|
import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
|
|
68
68
|
import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
@@ -313,6 +313,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
313
313
|
// Resolved per connection: the setup flow can install the binary and
|
|
314
314
|
// update acp.bin mid-session; the next turn picks it up (no restart).
|
|
315
315
|
bin: () => loadConfig().acp.bin,
|
|
316
|
+
// Registry-declared argv for the managed binary (e.g. linux --uid=).
|
|
317
|
+
// Empty for env/config overrides, which own their argv. Resolved per
|
|
318
|
+
// connection alongside bin so a mid-session install picks them up.
|
|
319
|
+
binArgs: () => resolveInstalledArgs({ configBin: loadConfig().acp.bin }),
|
|
316
320
|
// Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
|
|
317
321
|
// a restart. Without this the driver defaults to "estimate" and the
|
|
318
322
|
// config knob (incl. "off") is dead.
|
|
@@ -656,6 +660,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
656
660
|
);
|
|
657
661
|
const r = await runAcpAuth({
|
|
658
662
|
bin: status.bin,
|
|
663
|
+
binArgs: status.args,
|
|
659
664
|
...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
|
|
660
665
|
log: acpLog,
|
|
661
666
|
});
|
|
@@ -1319,6 +1324,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1319
1324
|
);
|
|
1320
1325
|
const r = await runAcpAuth({
|
|
1321
1326
|
bin: status.bin,
|
|
1327
|
+
binArgs: status.args,
|
|
1322
1328
|
extraEnv: ctx.authCapture?.browserEnv,
|
|
1323
1329
|
authUrlFile: ctx.authCapture?.file,
|
|
1324
1330
|
log: ctx.acpLog,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/acp/auth.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AcpConnection } from "./connection.js";
|
|
2
|
+
import { mapUpdate } from "./events.js";
|
|
2
3
|
import { hasAcpToken, writeAcpAuthVerified } from "./setup.js";
|
|
3
4
|
|
|
4
5
|
// Explicit sign-in (/agy auth): spawn a short-lived server, send
|
|
@@ -53,23 +54,62 @@ export interface AcpAuthResult {
|
|
|
53
54
|
error?: string;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
/** Google returns account-disablement as a normal session/prompt reply
|
|
58
|
+
* (stopReason end_turn) carrying this text, not a -32000 auth error, so a
|
|
59
|
+
* session/new probe alone cannot tell "authenticated" from "entitled". */
|
|
60
|
+
const ACCOUNT_DISABLED_RE = /disabled in this account|violation of\s+Terms of Service|submit an appeal/i;
|
|
61
|
+
|
|
62
|
+
/** Bounds the probe's session/prompt. A real first token normally lands well
|
|
63
|
+
* under this; a hollow or hung server hits the cap and fails auth. */
|
|
64
|
+
const PROBE_PROMPT_TIMEOUT_MS = 30_000;
|
|
65
|
+
|
|
66
|
+
/** Live credential probe for the token-absent branch. session/new alone is
|
|
67
|
+
* too shallow: a ToS-disabled account passes it. So the probe sends one
|
|
68
|
+
* trivial session/prompt and inspects the streamed reply (collected by the
|
|
69
|
+
* caller's onUpdate into `sink`): the disablement notice means the account
|
|
70
|
+
* is blocked (distinct from a hollow accept). Returns "usable" only when
|
|
71
|
+
* the prompt completes without that notice. Closes the probe session; the
|
|
72
|
+
* caller kills the connection either way. */
|
|
73
|
+
async function probeUsableCredentials(
|
|
74
|
+
conn: AcpConnection,
|
|
75
|
+
sink: { text: string },
|
|
76
|
+
log: (event: string, data?: unknown) => void,
|
|
77
|
+
cwd?: string,
|
|
78
|
+
): Promise<"usable" | "disabled" | "unusable"> {
|
|
79
|
+
let timer: NodeJS.Timeout | undefined;
|
|
61
80
|
try {
|
|
62
81
|
const s = await conn.newSession(cwd ?? process.cwd());
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
try {
|
|
83
|
+
sink.text = "";
|
|
84
|
+
// conn.prompt has no timeout by design (driver timers govern real
|
|
85
|
+
// turns). The probe runs during interactive auth, so bound it: a
|
|
86
|
+
// hollow server that accepts session/new but never answers the
|
|
87
|
+
// prompt must fail, not hang /agy auth and the authInFlight lock.
|
|
88
|
+
await Promise.race([
|
|
89
|
+
conn.prompt(s.sessionId, "ping"),
|
|
90
|
+
new Promise<never>((_, reject) => {
|
|
91
|
+
timer = setTimeout(() => reject(new Error("probe prompt timed out")), PROBE_PROMPT_TIMEOUT_MS);
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
await conn.closeSession(s.sessionId).catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
// Log the reply head so a change in Google's disablement wording (which
|
|
99
|
+
// would silently regress the regex to "usable") is diagnosable.
|
|
100
|
+
log("auth-probe-reply", { head: sink.text.slice(0, 200) });
|
|
101
|
+
return ACCOUNT_DISABLED_RE.test(sink.text) ? "disabled" : "usable";
|
|
65
102
|
} catch {
|
|
66
|
-
return
|
|
103
|
+
return "unusable";
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
let authInFlight: Promise<AcpAuthResult> | null = null;
|
|
71
108
|
|
|
72
109
|
export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
110
|
+
// Collects streamed reply text so the credential probe can detect an
|
|
111
|
+
// account-disablement notice (which arrives as normal prompt content).
|
|
112
|
+
const probeSink = { text: "" };
|
|
73
113
|
const conn = new AcpConnection({
|
|
74
114
|
bin: opts.bin,
|
|
75
115
|
binArgs: opts.binArgs,
|
|
@@ -77,7 +117,10 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
|
77
117
|
extraEnv: opts.extraEnv,
|
|
78
118
|
authUrlFile: opts.authUrlFile,
|
|
79
119
|
log: opts.log ?? (() => {}),
|
|
80
|
-
onUpdate: () => {
|
|
120
|
+
onUpdate: (_sessionId, update) => {
|
|
121
|
+
const mapped = mapUpdate(update);
|
|
122
|
+
if (mapped?.kind === "text") probeSink.text += mapped.delta;
|
|
123
|
+
},
|
|
81
124
|
onExit: () => {},
|
|
82
125
|
});
|
|
83
126
|
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
@@ -91,8 +134,15 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
|
91
134
|
if (!hasAcpToken(opts.tokenDir)) {
|
|
92
135
|
// authenticate replied but no token file: either the server
|
|
93
136
|
// authenticated from artifacts outside antigravity-acp, or the
|
|
94
|
-
// reply is hollow. A live session
|
|
95
|
-
|
|
137
|
+
// reply is hollow. A live session prompt separates the two.
|
|
138
|
+
const probe = await probeUsableCredentials(conn, probeSink, opts.log ?? (() => {}), opts.cwd);
|
|
139
|
+
if (probe === "disabled") {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: "this Google account is disabled for the Antigravity service (Terms of Service). Signing in again will not help; submit an appeal with Google to restore access.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (probe !== "usable") {
|
|
96
146
|
return {
|
|
97
147
|
ok: false,
|
|
98
148
|
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.`,
|
|
@@ -101,6 +151,11 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
|
|
|
101
151
|
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
102
152
|
return { ok: true, note: "the server authenticated from existing credentials; no browser sign-in was needed" };
|
|
103
153
|
}
|
|
154
|
+
// Token present (normal browser sign-in): no entitlement probe. A
|
|
155
|
+
// ToS-disabled account that completes a real sign-in still verifies
|
|
156
|
+
// here and surfaces the disablement on its first real prompt. The
|
|
157
|
+
// prompt probe is scoped to the token-absent branch (issue #9's
|
|
158
|
+
// `agy acp` shim path) to avoid a model call on every sign-in.
|
|
104
159
|
writeAcpAuthVerified(opts.authVerifiedFile);
|
|
105
160
|
return { ok: true };
|
|
106
161
|
} catch (err) {
|
package/src/acp/connection.ts
CHANGED
|
@@ -246,8 +246,13 @@ export class AcpConnection {
|
|
|
246
246
|
child.on("exit", (code, signal) => {
|
|
247
247
|
this.#opts.log("exit", { code: code ?? signal ?? "?" });
|
|
248
248
|
// The reason rides into pending-request rejections, so it must be
|
|
249
|
-
// redacted here (server stderr can carry auth material).
|
|
250
|
-
|
|
249
|
+
// redacted here (server stderr can carry auth material). When the
|
|
250
|
+
// server dies with no output the exit code/signal is the only clue,
|
|
251
|
+
// so it goes into the reason ahead of the stderr tail.
|
|
252
|
+
const where = signal ? `signal ${signal}` : code !== null ? `code ${code}` : "";
|
|
253
|
+
const tail = redactText(this.#stderrTail.trim());
|
|
254
|
+
const detail = [where, tail || "process gone"].filter(Boolean).join(": ");
|
|
255
|
+
this.#finish(detail, code, signal);
|
|
251
256
|
});
|
|
252
257
|
|
|
253
258
|
const result = (await this.request(
|
|
@@ -582,7 +587,7 @@ export class AcpConnection {
|
|
|
582
587
|
this.#opts.log("auth-url", { url, port: parseAuthPort(url) });
|
|
583
588
|
}
|
|
584
589
|
|
|
585
|
-
#finish(reason: string): void {
|
|
590
|
+
#finish(reason: string, code: number | null = null, signal: NodeJS.Signals | null = null): void {
|
|
586
591
|
if (this.#exited) return;
|
|
587
592
|
this.#exited = true;
|
|
588
593
|
this.#authUrlWatcher?.close();
|
|
@@ -593,7 +598,7 @@ export class AcpConnection {
|
|
|
593
598
|
this.#permissionPending.clear();
|
|
594
599
|
for (const deny of pendings) deny();
|
|
595
600
|
this.abortAll(`connection exited: ${reason || "process gone"}`);
|
|
596
|
-
this.#opts.onExit({ code
|
|
601
|
+
this.#opts.onExit({ code, signal, stderrTail: redactText(this.#stderrTail) });
|
|
597
602
|
}
|
|
598
603
|
}
|
|
599
604
|
|
package/src/acp/driver.ts
CHANGED
|
@@ -45,8 +45,10 @@ export interface AcpDriverOptions {
|
|
|
45
45
|
* is resolved per connection: setup can install the binary and update
|
|
46
46
|
* config mid-session, and the next turn picks it up without a restart. */
|
|
47
47
|
bin: string | (() => string);
|
|
48
|
-
/** Extra argv for the binary (tests: node + fake-server script).
|
|
49
|
-
|
|
48
|
+
/** Extra argv for the binary (tests: node + fake-server script). A
|
|
49
|
+
* function is resolved per connection, like bin, so registry args from a
|
|
50
|
+
* mid-session managed install apply without a restart. */
|
|
51
|
+
binArgs?: string[] | (() => string[]);
|
|
50
52
|
extraEnv?: Record<string, string>;
|
|
51
53
|
/** Record file for BROWSER-captured OAuth URLs; passed to the connection,
|
|
52
54
|
* which logs "auth-url" when the server tries to open a login. */
|
|
@@ -472,7 +474,7 @@ export class AcpDriver implements TurnDriver {
|
|
|
472
474
|
}
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
#onConnectionExit(conn: AcpConnection, info: { stderrTail: string }): void {
|
|
477
|
+
#onConnectionExit(conn: AcpConnection, info: { code: number | null; signal: string | null; stderrTail: string }): void {
|
|
476
478
|
if (this.#conn !== conn) {
|
|
477
479
|
// A replaced connection reporting its death late (RC01's signal
|
|
478
480
|
// handler intercepts SIGTERM and can outlive its replacement by
|
|
@@ -489,7 +491,12 @@ export class AcpDriver implements TurnDriver {
|
|
|
489
491
|
// mid-flight is user-reportable, and it surfaces as a normal turn
|
|
490
492
|
// error, not as raw console noise.
|
|
491
493
|
const turnActive = Boolean(turn && !turn.closed && !turn.aborted && !turn.sawResult);
|
|
492
|
-
this.#log("connection-exited", {
|
|
494
|
+
this.#log("connection-exited", {
|
|
495
|
+
expected: !turnActive,
|
|
496
|
+
code: info.code,
|
|
497
|
+
signal: info.signal,
|
|
498
|
+
tail: info.stderrTail.slice(-200),
|
|
499
|
+
});
|
|
493
500
|
if (!turn || turn.closed) return;
|
|
494
501
|
if (turn.aborted || turn.sawResult) {
|
|
495
502
|
this.#settle(turn, {
|
|
@@ -501,7 +508,10 @@ export class AcpDriver implements TurnDriver {
|
|
|
501
508
|
});
|
|
502
509
|
return;
|
|
503
510
|
}
|
|
504
|
-
|
|
511
|
+
// A silent death (no stderr) leaves only the exit code/signal as a
|
|
512
|
+
// clue; surface it so the failure is not a bare "exited unexpectedly".
|
|
513
|
+
const where = info.signal ? ` (signal ${info.signal})` : info.code !== null ? ` (code ${info.code})` : "";
|
|
514
|
+
this.#failTurn(turn, `Antigravity session exited unexpectedly${where}; it restarts on the next turn`);
|
|
505
515
|
}
|
|
506
516
|
|
|
507
517
|
#ensureConnection(request: DriverTurnRequest): Promise<AcpConnection> {
|
|
@@ -511,7 +521,7 @@ export class AcpDriver implements TurnDriver {
|
|
|
511
521
|
this.#stats.spawns += 1;
|
|
512
522
|
const conn = new AcpConnection({
|
|
513
523
|
bin: resolveAcpBinary(typeof this.#opts.bin === "function" ? this.#opts.bin() : this.#opts.bin),
|
|
514
|
-
binArgs: this.#opts.binArgs,
|
|
524
|
+
binArgs: typeof this.#opts.binArgs === "function" ? this.#opts.binArgs() : this.#opts.binArgs,
|
|
515
525
|
extraEnv: this.#opts.extraEnv,
|
|
516
526
|
authUrlFile: this.#opts.authUrlFile,
|
|
517
527
|
cwd: request.cwd,
|
package/src/acp/setup.ts
CHANGED
|
@@ -52,6 +52,8 @@ export type AcpSetupStatus =
|
|
|
52
52
|
ok: true;
|
|
53
53
|
bin: string;
|
|
54
54
|
binarySource: "env" | "config" | "installed" | "existing";
|
|
55
|
+
/** Registry argv for the managed binary; empty for env/config overrides. */
|
|
56
|
+
args: string[];
|
|
55
57
|
auth: string;
|
|
56
58
|
/** True when auth was just set to oauth-personal: /agy auth opens
|
|
57
59
|
* the Google login in the browser. */
|
|
@@ -121,6 +123,15 @@ export function executableFile(p: string): boolean {
|
|
|
121
123
|
interface RegistryBinary {
|
|
122
124
|
archive: string;
|
|
123
125
|
cmd?: string;
|
|
126
|
+
/** Platform-specific argv the server needs to boot (e.g. linux `--uid=`).
|
|
127
|
+
* Untrusted registry data: validated as a string array before use. */
|
|
128
|
+
args?: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Registry args are untrusted; accept only a flat string array. */
|
|
132
|
+
function normalizeRegistryArgs(raw: unknown): string[] {
|
|
133
|
+
if (!Array.isArray(raw)) return [];
|
|
134
|
+
return raw.filter((a): a is string => typeof a === "string");
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
async function fetchRegistryBinary(opts: SetupOptions, key: string): Promise<RegistryBinary> {
|
|
@@ -180,6 +191,8 @@ export interface InstallResult {
|
|
|
180
191
|
build: string;
|
|
181
192
|
sha256: string;
|
|
182
193
|
downloaded: boolean;
|
|
194
|
+
/** Registry-declared argv for the managed binary (may be empty). */
|
|
195
|
+
args: string[];
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
/** Fetch the pinned registry build for this platform and install it under
|
|
@@ -193,8 +206,11 @@ export async function installAcpBinary(opts: SetupOptions = {}): Promise<Install
|
|
|
193
206
|
const cmd = (entry.cmd ?? "./agy_acp_server.par").replace(/^\.\//, "");
|
|
194
207
|
const dir = path.join(root, build);
|
|
195
208
|
const bin = path.join(dir, cmd);
|
|
209
|
+
const args = normalizeRegistryArgs(entry.args);
|
|
196
210
|
if (executableFile(bin)) {
|
|
197
|
-
|
|
211
|
+
// Already installed: recover any recorded args so the early-return
|
|
212
|
+
// path does not silently drop them.
|
|
213
|
+
return { bin, build, sha256: readRecordedSha(dir), downloaded: false, args: readRecordedArgs(dir) };
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
fs.mkdirSync(root, { recursive: true });
|
|
@@ -209,13 +225,14 @@ export async function installAcpBinary(opts: SetupOptions = {}): Promise<Install
|
|
|
209
225
|
if (!executableFile(bin)) throw new Error(`${cmd} not found in the archive after unpacking`);
|
|
210
226
|
fs.chmodSync(bin, 0o755);
|
|
211
227
|
fs.writeFileSync(path.join(dir, "zip.sha256"), `${sha} ${path.basename(entry.archive)}\n`);
|
|
228
|
+
if (args.length > 0) fs.writeFileSync(path.join(dir, "args.json"), JSON.stringify(args));
|
|
212
229
|
|
|
213
230
|
// Re-point `current` at the new build. The link is ours (managed layout).
|
|
214
231
|
const link = path.join(root, "current");
|
|
215
232
|
fs.rmSync(link, { force: true });
|
|
216
233
|
fs.symlinkSync(build, link);
|
|
217
234
|
opts.onProgress?.("ACP server installed.");
|
|
218
|
-
return { bin, build, sha256: sha, downloaded: true };
|
|
235
|
+
return { bin, build, sha256: sha, downloaded: true, args };
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
function readRecordedSha(dir: string): string {
|
|
@@ -226,6 +243,14 @@ function readRecordedSha(dir: string): string {
|
|
|
226
243
|
}
|
|
227
244
|
}
|
|
228
245
|
|
|
246
|
+
function readRecordedArgs(dir: string): string[] {
|
|
247
|
+
try {
|
|
248
|
+
return normalizeRegistryArgs(JSON.parse(fs.readFileSync(path.join(dir, "args.json"), "utf8")));
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
229
254
|
export interface AuthState {
|
|
230
255
|
configured: boolean;
|
|
231
256
|
/** settings.json auth.type, "token" (acp_token.json present), or undefined. */
|
|
@@ -343,6 +368,25 @@ export function resolveInstalledBinary(opts: SetupOptions = {}): BinaryResolutio
|
|
|
343
368
|
return { bin: null, source: null };
|
|
344
369
|
}
|
|
345
370
|
|
|
371
|
+
/** Registry argv for the managed install only. Env/config overrides (a user
|
|
372
|
+
* shim like `agy acp`, or AGY_ACP_BIN) own their argv, so they get none.
|
|
373
|
+
* Identity is by PATH, not by which candidate won: setup persists the managed
|
|
374
|
+
* path into acp.bin, so after install the config candidate resolves to the
|
|
375
|
+
* managed binary too. Compare realpaths against `<root>/current/...` (the
|
|
376
|
+
* symlink target carries the args.json sidecar). Empty when not managed. */
|
|
377
|
+
export function resolveInstalledArgs(opts: SetupOptions = {}): string[] {
|
|
378
|
+
const binary = resolveInstalledBinary(opts);
|
|
379
|
+
if (!binary.bin) return [];
|
|
380
|
+
const root = opts.installRoot ?? defaultInstallRoot();
|
|
381
|
+
const managed = path.join(root, "current", "agy_acp_server.par");
|
|
382
|
+
try {
|
|
383
|
+
if (fs.realpathSync(binary.bin) !== fs.realpathSync(managed)) return [];
|
|
384
|
+
} catch {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
return readRecordedArgs(path.dirname(fs.realpathSync(managed)));
|
|
388
|
+
}
|
|
389
|
+
|
|
346
390
|
/** Read-only setup state (for /agy doctor): no installs, no writes. */
|
|
347
391
|
export function inspectAcpSetup(opts: SetupOptions = {}): { bin: string | null; source: string | null; auth: string | null } {
|
|
348
392
|
const binary = resolveInstalledBinary(opts);
|
|
@@ -367,6 +411,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
367
411
|
|
|
368
412
|
// 1. Binary.
|
|
369
413
|
let binary = resolveInstalledBinary(opts);
|
|
414
|
+
let args = resolveInstalledArgs(opts);
|
|
370
415
|
if (!binary.bin) {
|
|
371
416
|
if (opts.install === false) {
|
|
372
417
|
return { ok: false, stage: "install", error: "no ACP server binary found", manual: MANUAL_SETUP };
|
|
@@ -374,6 +419,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
374
419
|
try {
|
|
375
420
|
const installed = await installAcpBinary(opts);
|
|
376
421
|
binary = { bin: installed.bin, source: "installed" };
|
|
422
|
+
args = installed.args;
|
|
377
423
|
actions.push(installed.downloaded ? `installed ACP server build ${installed.build}` : `found installed ACP server build ${installed.build}`);
|
|
378
424
|
} catch (err) {
|
|
379
425
|
return {
|
|
@@ -397,7 +443,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
397
443
|
try {
|
|
398
444
|
writeAuthType("oauth-personal", gdir);
|
|
399
445
|
actions.push("configured oauth-personal auth (your Antigravity subscription login)");
|
|
400
|
-
return { ok: true, bin: binary.bin!, binarySource: binary.source!, auth: "oauth-personal", needsLogin: true, actions };
|
|
446
|
+
return { ok: true, bin: binary.bin!, binarySource: binary.source!, args, auth: "oauth-personal", needsLogin: true, actions };
|
|
401
447
|
} catch (err) {
|
|
402
448
|
return {
|
|
403
449
|
ok: false,
|
|
@@ -419,6 +465,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
|
|
|
419
465
|
ok: true,
|
|
420
466
|
bin: binary.bin!,
|
|
421
467
|
binarySource: binary.source!,
|
|
468
|
+
args,
|
|
422
469
|
auth: auth.type ?? "token",
|
|
423
470
|
needsLogin,
|
|
424
471
|
actions,
|