@estebanforge/pi-antigravity-bridge 1.5.0 → 1.5.1
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 +11 -0
- package/README.md +11 -6
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/extensions/index.ts +37 -20
- package/package.json +1 -1
- package/src/ask-tool.ts +39 -0
- package/src/mcp-registration.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.5.1] - 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Delegated agy no longer sees the pi tool bridge. `AskAntigravity` spawns `agy -p`, and any agy on the machine discovers MCP servers from the global `~/.gemini/config/mcp_config.json`, which carries a live `pi-bridge-*` entry for every running pi session. A delegation could therefore connect to a host bridge and call tools (observed live: `memory_search`), which the round-trip store denies fail-closed with "no active antigravity turn" the moment no provider turn is streaming - a dead end for the delegation and an error toast in the host session. The bridge entries are now suppressed while a delegated agy starts: disabled before the spawn, re-enabled on process close or after a 5s grace, whichever lands first. Suppression is reference-counted, so overlapping delegations cannot re-enable early; session start re-enables any entries a crashed delegation left disabled. Foreign MCP servers in the file are never touched. Residual race, documented in code: another session's provider agy respawning inside the window reads the entries disabled and that process lacks bridge tools until its next recycle.
|
|
10
|
+
- Real bridge tool-call failures now land on disk in default mode. The daily log records only errors, and `call-tool-fail` was logged at warn tier (toast only), so a failure like the one above left no durable trace. Genuine rejections now log at error tier; routine turn-end and shutdown aborts stay silent, with the reasons shared as constants between the emit sites and the log classifier so the two cannot drift.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- README corrected: the bridge mechanism section described only the dir-scoped discovery and claimed the user's global agy config is never touched. Global per-pid registration shipped earlier and is now documented, along with the delegation suppression and its window.
|
|
15
|
+
|
|
5
16
|
## [1.5.0] - 2026-09-08
|
|
6
17
|
|
|
7
18
|
### Added
|
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A Gemini model provider **and** the `AskAntigravity` delegation tool for [pi](https://github.com/earendil-works/pi-coding-agent), built on Google's official Antigravity binaries: the `agy` CLI by default, or **Google's official ACP server** (opt-in). It registers `antigravity/gemini-*` models in pi's `/model` picker (streaming), and provides the `AskAntigravity` tool for one-shot delegation - the same combined shape as `pi-claude-bridge`.
|
|
4
4
|
|
|
5
|
-
<
|
|
5
|
+
<p>
|
|
6
|
+
<img width="1374" height="869" alt="image" src="https://github.com/user-attachments/assets/fdd6c1f0-5144-40ef-9371-77a94de50b76" align="center" />
|
|
7
|
+
</p>
|
|
8
|
+
<p>
|
|
9
|
+
<img width="1376" height="451" alt="SCR-20260908-mbox" src="https://github.com/user-attachments/assets/d105c8a7-1ace-42f7-8285-642150453761" align="center" />
|
|
10
|
+
</p>
|
|
11
|
+
<p>
|
|
12
|
+
<img width="1375" height="1342" alt="SCR-20260908-mbvc" src="https://github.com/user-attachments/assets/bb614c55-e0f8-4677-bb0e-6541071e91ef" align="center" />
|
|
13
|
+
</p>
|
|
6
14
|
|
|
7
|
-
<br/><br/>
|
|
8
|
-
|
|
9
|
-
<img width="1272" height="842" alt="SCR-20260903-sfcu" src="https://github.com/user-attachments/assets/c483613b-10ff-4d2f-9893-027d84130ac6" align="center" />
|
|
10
15
|
|
|
11
16
|
If you also have [`@estebanforge/pi-ask-antigravity`](https://github.com/EstebanForge/pi-ask-antigravity) installed, this bridge takes over: pi-ask-antigravity detects the bridge and registers nothing, so the `AskAntigravity` tool is never duplicated.
|
|
12
17
|
|
|
@@ -43,13 +48,13 @@ Residual limits (with or without the bridge):
|
|
|
43
48
|
|
|
44
49
|
While agy is the active model it normally cannot see pi's universe of extensions: agentmemory, codegraph, web search, slack/asana, the `Ask*` delegations, and any other installed pi tool. This extension optionally bridges that gap.
|
|
45
50
|
|
|
46
|
-
The bridge starts a localhost MCP server inside pi's process. `tools/list` returns pi's registered tools (built-in file/shell tools and `AskAntigravity` are filtered out), and a `tools/call` routes into pi's own tool loop via the round-trip described below.
|
|
51
|
+
The bridge starts a localhost MCP server inside pi's process. `tools/list` returns pi's registered tools (built-in file/shell tools and `AskAntigravity` are filtered out), and a `tools/call` routes into pi's own tool loop via the round-trip described below. Discovery has two layers. The provider's agy gets a per-invocation config: the bridge writes `.agents/mcp_config.json` into a bridge-controlled dir (`~/.pi/agent/antigravity-bridge/agy-mcp-<pid>/`) and the driver passes that dir as an extra `--add-dir` when it spawns agy. A per-pid entry (`pi-bridge-<pid>`) is also registered in the user's global agy config (`~/.gemini/config/mcp_config.json`; foreign servers preserved, stale entries swept at start), so agy builds that read only the global config still find the bridge.
|
|
47
52
|
|
|
48
53
|
**No patch required.** Bridge calls park in the provider's round-trip store; the provider ends the pi assistant message with a `toolUse` stop reason for the real pi tool, pi executes it in its own loop (native cards, permissions, hooks), and the toolResult completes the parked MCP response on the next stream call. This is the same mechanism tianzuo/pi-antigravity uses; upstream pi APIs only.
|
|
49
54
|
|
|
50
55
|
**Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost.
|
|
51
56
|
|
|
52
|
-
**Recursion safety.** Only the provider's agy receives the extra `--add-dir`.
|
|
57
|
+
**Recursion safety.** Only the provider's agy receives the extra `--add-dir`. `AskAntigravity` is also filtered from the exposed tool list. Its delegated `agy -p` spawns with just the workspace, and while it starts the bridge suppresses its global per-pid entries: disabled before the spawn, re-enabled on process close or after a 5s grace (whichever lands first), refcounted across overlapping delegations, and re-enabled at every session start. The inner agy therefore sees no bridge and cannot re-enter pi. A hand-run agy outside pi that starts inside the seconds-wide window briefly misses the bridge; foreign servers in the global config are never touched.
|
|
53
58
|
|
|
54
59
|
**Cost / fan-out.** Every registered pi tool except builtins (and `AskAntigravity`) is exposed, including other delegation tools like `AskClaude`/`AskCodex`. agy can therefore chain into other models via the bridge, which is a new cost/time fan-out vector that did not exist before this feature.
|
|
55
60
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -27,7 +27,7 @@ src/config.ts persisted runtime config (engine + acp block, bridgeTools,
|
|
|
27
27
|
src/daily-log.ts daily NDJSON support log (one file per day, 14-day retention, secret redaction, AGY_DEBUG verbose gate); fed by both drivers, the bridge, round-trips, /agy, and ask-tool
|
|
28
28
|
src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
|
|
29
29
|
src/mcp-server.ts MCP tool bridge server: ferries tools/list + tools/call; calls park in the provider round-trip. Also the approval park: POST /approval (ticket early-ack) + GET /approval/<id>, fail-closed on timeout/unwired/close
|
|
30
|
-
src/mcp-registration.ts registers/unregisters the bridge in ~/.gemini/config/mcp_config.json for the stream-json CLI (per-pid, atomic, stale sweep)
|
|
30
|
+
src/mcp-registration.ts registers/unregisters the bridge in ~/.gemini/config/mcp_config.json for the stream-json CLI (per-pid, atomic, stale sweep) and flips our entries off around AskAntigravity spawns (refcounted; session start re-enables)
|
|
31
31
|
src/approval-gate.ts shadow tool factory (bash/write/edit): marker calls are ticket-verified approval round-trips, non-marker calls delegate; maps agy native tools onto the shadow surface
|
|
32
32
|
src/approval-detect.ts third-party pi permission-extension detection; resolves approvals.gateMode auto (off until a gate extension exists)
|
|
33
33
|
src/approval-hook.ts merge-safe .agents/hooks.json staging (PreToolUse) + generated 0600 early-ack/poll hook script; staged timeout exceeds the park budget (hook timeouts soft-pass)
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -98,7 +98,7 @@ Most "stuck" reports trace to one of:
|
|
|
98
98
|
- `tests/acp-driver.test.ts` - the ACP driver over the fake server (`tests/helpers/fake-acp-server.mjs`, scenario-selected): happy flow, load-replay, permission auto-answer, Gate D abort (cancel probe, teardown, `cancelSupported` memory), the stale-exit race (a killed connection's late exit must not fail its replacement - `ACP_FAKE_SLOW_DEATH_MS`), auth errors, park/kickIdle timer pause with remaining budget.
|
|
99
99
|
- `tests/acp-config.test.ts` - engine selection narrowing (`AGY_ENGINE`/`config.engine`), acp block parsing.
|
|
100
100
|
- `tests/daily-log.test.ts` - the support log: day rotation, retention cutoff boundary, secret redaction (incl. header blocks), the 4 KB record cap, never-throw on a broken dir, and the two-tier gate (debug records dropped unless `AGY_DEBUG`).
|
|
101
|
-
- `tests/mcp-registration.test.ts` - the `~/.gemini/config/mcp_config.json` registration for the stream-json CLI: exact agy entry shape, foreign servers preserved, corrupt config refused, atomic writes, stale-entry sweep.
|
|
101
|
+
- `tests/mcp-registration.test.ts` - the `~/.gemini/config/mcp_config.json` registration for the stream-json CLI: exact agy entry shape, foreign servers preserved, corrupt config refused, atomic writes, stale-entry sweep, delegation suppression (`setBridgeEntriesDisabled` flip/restore/idempotence, refcounted `acquireBridgeSuppression`).
|
|
102
102
|
- `tests/engine-picker.test.ts` - first-run onboarding: picker gate (no config file + no AGY_ENGINE, fail-closed fs), option order (stream-json first), the ACP download/sign-in disclosure pins (intro + saved toast), agy binary detection (PATH scan, explicit path, fail-closed), missing-CLI toast copy.
|
|
103
103
|
- `tests/provider-escalation.test.ts` - the early-ack + poll pipeline (escalation registry, poll views, late-delivery tombstones) and tool-result image forwarding on both engines.
|
|
104
104
|
- `tests/approval-gate.test.ts` - the shadow tool factory: marker calls never execute, ticket verification denies forged/stale markers before the policy, denials throw, native-to-shadow mapping.
|
package/extensions/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.j
|
|
|
72
72
|
import { startMcpServer, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
|
|
73
73
|
import {
|
|
74
74
|
registerBridgeServer,
|
|
75
|
+
setBridgeEntriesDisabled,
|
|
75
76
|
sweepStaleBridgeServers,
|
|
76
77
|
unregisterBridgeServer,
|
|
77
78
|
} from "../src/mcp-registration.js";
|
|
@@ -131,6 +132,12 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
131
132
|
const modelInput: Array<"text" | "image"> = engine === "acp" ? ["text", "image"] : ["text"];
|
|
132
133
|
const models = entries.map((e) => toPiModel(e, modelInput));
|
|
133
134
|
|
|
135
|
+
// failAll reasons the bridge treats as routine (turn end / shutdown
|
|
136
|
+
// sweep), not faults. Shared by the emitters below and the mcpLog
|
|
137
|
+
// classifier so the substring match cannot drift from the text.
|
|
138
|
+
const FAIL_REASON_TURN_END = "antigravity turn ended with an unresolved pi tool call";
|
|
139
|
+
const FAIL_REASON_SHUTDOWN = "antigravity session shut down";
|
|
140
|
+
|
|
134
141
|
// Daily file log: every sink below feeds ~/.pi/extensions-data/
|
|
135
142
|
// estebanforge/pi-antigravity-bridge/logs/<YYYY-MM-DD>.ndjson (see
|
|
136
143
|
// src/daily-log.ts). Fire-and-forget, secrets redacted, old days pruned.
|
|
@@ -140,14 +147,15 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
140
147
|
// routine disk traffic), so a warn that never reaches the UI is lost.
|
|
141
148
|
// Every warn toasts here instead; AGY_DEBUG=1 restores the full file
|
|
142
149
|
// trail. Silent exceptions: deliberate aborts (pi already shows
|
|
143
|
-
// "Operation aborted")
|
|
144
|
-
// carries real crashes)
|
|
145
|
-
//
|
|
150
|
+
// "Operation aborted") and connection exits (the turn's own error block
|
|
151
|
+
// carries real crashes). call-tool-fail needs no exclusion here: its tier
|
|
152
|
+
// is debug or error, never warn. No UI (headless): warn text falls back
|
|
153
|
+
// to stderr.
|
|
146
154
|
const rawFileLog = fileLog.log.bind(fileLog);
|
|
147
155
|
fileLog.log = (event, data, level) => {
|
|
148
156
|
rawFileLog(event, data, level);
|
|
149
157
|
if (level !== "warn") return;
|
|
150
|
-
if (event.startsWith("abort:") || event === "connection-exited"
|
|
158
|
+
if (event.startsWith("abort:") || event === "connection-exited") return;
|
|
151
159
|
const d = (data ?? {}) as Record<string, unknown>;
|
|
152
160
|
let text: string;
|
|
153
161
|
if (event === "round-trip-fail") {
|
|
@@ -341,7 +349,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
341
349
|
{ status: outcome.status, error: outcome.error?.slice(0, 500), aborted: outcome.aborted },
|
|
342
350
|
outcome.status === "OK" ? "info" : "warn",
|
|
343
351
|
);
|
|
344
|
-
roundTrips.failAll(
|
|
352
|
+
roundTrips.failAll(FAIL_REASON_TURN_END);
|
|
345
353
|
};
|
|
346
354
|
streamDriver.onTurnEnd = onTurnEnd;
|
|
347
355
|
acpDriver.onTurnEnd = onTurnEnd;
|
|
@@ -593,18 +601,27 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
593
601
|
// filters below only decide what reaches the user. Bridge calls
|
|
594
602
|
// start/end at info (one record per tool call, the fragile-path
|
|
595
603
|
// skeleton); list-tools and startup chatter stay verbose.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
604
|
+
// Routine abort traffic (failAll on turn end / session shutdown
|
|
605
|
+
// answers every parked call with an error) is not a fault: debug
|
|
606
|
+
// only. Any other call-tool-fail is a real rejection (e.g. "no
|
|
607
|
+
// active antigravity turn" from a client that should not see the
|
|
608
|
+
// bridge) and lands at error tier - default mode records errors
|
|
609
|
+
// only, so this is the sole durable trace of the incident.
|
|
610
|
+
const detail = (d as { msg?: string } | undefined)?.msg ?? "";
|
|
611
|
+
const routineAbort =
|
|
612
|
+
s === "call-tool-fail" &&
|
|
613
|
+
(detail.includes(FAIL_REASON_TURN_END) || detail.includes(FAIL_REASON_SHUTDOWN));
|
|
614
|
+
const level = !failures.has(s)
|
|
615
|
+
? s === "call-tool" || s === "call-tool-ok"
|
|
599
616
|
? "info"
|
|
600
|
-
: "debug"
|
|
617
|
+
: "debug"
|
|
618
|
+
: routineAbort
|
|
619
|
+
? "debug"
|
|
620
|
+
: s === "call-tool-fail"
|
|
621
|
+
? "error"
|
|
622
|
+
: "warn";
|
|
601
623
|
fileLog.log(s, d, level);
|
|
602
|
-
|
|
603
|
-
// and the bridge answers every parked call with an error. Not a fault.
|
|
604
|
-
if (s === "call-tool-fail") {
|
|
605
|
-
const detail = (d as { msg?: string } | undefined)?.msg ?? "";
|
|
606
|
-
if (detail.includes("unresolved pi tool call") || detail.includes("session shut down")) return;
|
|
607
|
-
}
|
|
624
|
+
if (routineAbort) return;
|
|
608
625
|
if (!failures.has(s)) return;
|
|
609
626
|
const msg = `[antigravity-bridge mcp] ${s}${d !== undefined ? " " + JSON.stringify(d) : ""}`;
|
|
610
627
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
@@ -708,11 +725,11 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
708
725
|
);
|
|
709
726
|
if (r.ok && r.handle) {
|
|
710
727
|
mcpHandle = r.handle;
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
// removed at session_shutdown; stale entries swept at start.
|
|
728
|
+
// Stale entries swept at start; entries a crashed delegation left
|
|
729
|
+
// suppressed are healed here (live bridges start every session
|
|
730
|
+
// enabled).
|
|
715
731
|
sweepStaleBridgeServers();
|
|
732
|
+
setBridgeEntriesDisabled(false);
|
|
716
733
|
registerBridgeServer({
|
|
717
734
|
pid: process.pid,
|
|
718
735
|
port: r.handle.port,
|
|
@@ -816,7 +833,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
816
833
|
const h = mcpHandle;
|
|
817
834
|
mcpHandle = null;
|
|
818
835
|
await h?.close();
|
|
819
|
-
roundTrips.failAll(
|
|
836
|
+
roundTrips.failAll(FAIL_REASON_SHUTDOWN);
|
|
820
837
|
// "recycle", NOT "shutdown": pi fires session_shutdown on /new, /resume
|
|
821
838
|
// and /fork (docs/extensions.md session lifecycle), not only on process
|
|
822
839
|
// exit. The drivers are process-lifetime singletons; closing them with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
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/ask-tool.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
snapshotConversations,
|
|
26
26
|
} from "./discovery.js";
|
|
27
27
|
import { loadConfig, type AgyMode, type ThinkingTier } from "./config.js";
|
|
28
|
+
import { acquireBridgeSuppression } from "./mcp-registration.js";
|
|
28
29
|
import { spawnAgyModelsRaw } from "./models.js";
|
|
29
30
|
|
|
30
31
|
// --- Constants -------------------------------------------------------------
|
|
@@ -46,6 +47,15 @@ const FAMILY_DEFAULT_TIER: Record<Family, ThinkingTier> = {
|
|
|
46
47
|
pro: "high",
|
|
47
48
|
other: "medium",
|
|
48
49
|
};
|
|
50
|
+
|
|
51
|
+
/** Bridge-suppression grace for a delegated `agy -p`. The shared config stays
|
|
52
|
+
* disabled for min(process close, this): agy reads it once at startup, so
|
|
53
|
+
* this bound covers that read without staying disabled for the whole run.
|
|
54
|
+
* Residual race: a provider agy respawn in ANOTHER session landing inside
|
|
55
|
+
* the window reads the entries disabled and that process lacks bridge tools
|
|
56
|
+
* until its next recycle (session start heals the file). Fail-open past the
|
|
57
|
+
* window = status-quo behavior. */
|
|
58
|
+
const BRIDGE_SUPPRESS_MS = 5000;
|
|
49
59
|
const TIER_RANK: Record<ThinkingTier, number> = { low: 0, medium: 1, high: 2 };
|
|
50
60
|
|
|
51
61
|
// Static alias overlay for non-Gemini models agy may or may not surface.
|
|
@@ -510,6 +520,31 @@ export async function registerAskAntigravityTool(
|
|
|
510
520
|
const binary = process.env.AGY_BIN || "agy";
|
|
511
521
|
let out = "";
|
|
512
522
|
|
|
523
|
+
// Delegation isolation: any agy on this machine reads the global
|
|
524
|
+
// mcp_config.json, so this spawned `agy -p` would discover live
|
|
525
|
+
// pi-bridge-* entries and call tools the host bridge cannot serve
|
|
526
|
+
// outside a live provider turn ("no active antigravity turn").
|
|
527
|
+
// Delegation isolation: any agy on this machine reads the global
|
|
528
|
+
// mcp_config.json, so this spawned `agy -p` would discover live
|
|
529
|
+
// pi-bridge-* entries and call tools the host bridge cannot serve
|
|
530
|
+
// outside a live provider turn ("no active antigravity turn").
|
|
531
|
+
// Window = min(close, grace): the release fires on process close or
|
|
532
|
+
// after BRIDGE_SUPPRESS_MS, whichever lands first. Refcounted, so an
|
|
533
|
+
// overlapping delegation cannot re-enable early. A refused config
|
|
534
|
+
// fail-opens to the status quo.
|
|
535
|
+
const releaseBridge = acquireBridgeSuppression();
|
|
536
|
+
let suppressTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(
|
|
537
|
+
releaseBridge,
|
|
538
|
+
BRIDGE_SUPPRESS_MS,
|
|
539
|
+
);
|
|
540
|
+
const restoreBridge = (): void => {
|
|
541
|
+
if (suppressTimer) {
|
|
542
|
+
clearTimeout(suppressTimer);
|
|
543
|
+
suppressTimer = undefined;
|
|
544
|
+
}
|
|
545
|
+
releaseBridge();
|
|
546
|
+
};
|
|
547
|
+
|
|
513
548
|
const statusInterval = onUpdate
|
|
514
549
|
? setInterval(() => {
|
|
515
550
|
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
@@ -592,6 +627,7 @@ export async function registerAskAntigravityTool(
|
|
|
592
627
|
if (watchdog) clearTimeout(watchdog);
|
|
593
628
|
if (sigkillTimer) clearTimeout(sigkillTimer);
|
|
594
629
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
630
|
+
restoreBridge();
|
|
595
631
|
};
|
|
596
632
|
const onAbort = () => killTree();
|
|
597
633
|
|
|
@@ -698,6 +734,9 @@ export async function registerAskAntigravityTool(
|
|
|
698
734
|
return { content: [{ type: "text", text: `failed to run agy: ${msg}` }], details };
|
|
699
735
|
}
|
|
700
736
|
finally {
|
|
737
|
+
// Belt and braces: cleanup() already restores on close/error; this
|
|
738
|
+
// covers paths that never reached the process (sync spawn throw).
|
|
739
|
+
restoreBridge();
|
|
701
740
|
if (contextFile) {
|
|
702
741
|
try {
|
|
703
742
|
fs.unlinkSync(contextFile);
|
package/src/mcp-registration.ts
CHANGED
|
@@ -93,6 +93,61 @@ export function unregisterBridgeServer(pid: number, configPath: string = mcpConf
|
|
|
93
93
|
return { wrote: true };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Flip `disabled` on every pi-bridge-* entry (foreign servers untouched).
|
|
97
|
+
*
|
|
98
|
+
* (1) Delegation isolation: the global config is read by ANY agy on the
|
|
99
|
+
* machine, so an `agy -p` we spawn ourselves (AskAntigravity) would discover
|
|
100
|
+
* live bridge entries and call tools the round-trip store cannot serve
|
|
101
|
+
* outside a live provider turn (fail-closed "no active antigravity turn",
|
|
102
|
+
* observed live 2026-09-15). agy reads the config once at startup, so a
|
|
103
|
+
* short suppression window around the spawn hides the bridge from it.
|
|
104
|
+
*
|
|
105
|
+
* (2) Startup healing (disabled=false): clears entries a crashed delegation
|
|
106
|
+
* left suppressed. */
|
|
107
|
+
export function setBridgeEntriesDisabled(
|
|
108
|
+
disabled: boolean,
|
|
109
|
+
configPath: string = mcpConfigPath(),
|
|
110
|
+
): { wrote: boolean; changed: number; reason?: string } {
|
|
111
|
+
const read = readConfig(configPath);
|
|
112
|
+
if (!read.ok) return { wrote: false, changed: 0, reason: read.reason };
|
|
113
|
+
let changed = 0;
|
|
114
|
+
for (const [name, entry] of Object.entries(read.config.mcpServers)) {
|
|
115
|
+
if (!/^pi-bridge-\d+$/.test(name)) continue;
|
|
116
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
117
|
+
const e = entry as { disabled?: unknown };
|
|
118
|
+
if ((e.disabled === true) === disabled) continue;
|
|
119
|
+
e.disabled = disabled;
|
|
120
|
+
changed++;
|
|
121
|
+
}
|
|
122
|
+
if (changed === 0) return { wrote: false, changed: 0 };
|
|
123
|
+
writeConfig(configPath, read.config);
|
|
124
|
+
return { wrote: true, changed };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const suppressionRefs = new Map<string, number>();
|
|
128
|
+
|
|
129
|
+
/** Reference-counted suppression for a self-spawned agy process (AskAntigravity
|
|
130
|
+
* delegation). First acquire disables every pi-bridge-* entry, last release
|
|
131
|
+
* re-enables; nested acquires are free, so overlapping delegations in one
|
|
132
|
+
* process cannot clobber each other's window. Same-process only: delegations
|
|
133
|
+
* from two pi sessions still race on the shared file - accepted, fail-open
|
|
134
|
+
* to the status-quo error. */
|
|
135
|
+
export function acquireBridgeSuppression(configPath: string = mcpConfigPath()): () => void {
|
|
136
|
+
const key = path.resolve(configPath);
|
|
137
|
+
const refs = (suppressionRefs.get(key) ?? 0) + 1;
|
|
138
|
+
suppressionRefs.set(key, refs);
|
|
139
|
+
if (refs === 1) setBridgeEntriesDisabled(true, configPath);
|
|
140
|
+
let released = false;
|
|
141
|
+
return () => {
|
|
142
|
+
if (released) return;
|
|
143
|
+
released = true;
|
|
144
|
+
const left = Math.max(0, (suppressionRefs.get(key) ?? 1) - 1);
|
|
145
|
+
if (left === 0) suppressionRefs.delete(key);
|
|
146
|
+
else suppressionRefs.set(key, left);
|
|
147
|
+
if (left === 0) setBridgeEntriesDisabled(false, configPath);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
/** Default liveness probe: can the signal be delivered? */
|
|
97
152
|
function pidAlive(pid: number): boolean {
|
|
98
153
|
try {
|