Imported from EstebanForge/pi-extensions (
packages/pi-antigravity-bridge/AGENTS.md). Install upstream withnpx skills add EstebanForge/pi-extensions --skill pi-antigravity-bridge. Copyright stays with the author.
Project: @estebanforge/pi-antigravity-bridge
Stack: TypeScript / Node.js (ESNext / ES2022, ESM module) targeting Node.js 22+, built as a streaming Gemini provider extension for pi (@earendil-works/pi-coding-agent) with TWO turn engines behind one contract: the default stream-json engine (persistent agy CLI process) and the opt-in ACP engine (src/acp/*, Google's official agy_acp_server.par over JSON-RPC stdio, disabled by default).
STRUCTURE
src/: Core implementation modulesprovider.ts: Mainpicustom provider: streaming event loop, G9 no-patch round-trip store (escalation-aware: slow bridge calls early-ack with a poll handle at ~20s and re-arm to a 30 min TTL; failed parks leave tombstones for late-delivery re-routing), G1 context digest (off by default; inline on stream-json,embeddedContextresource block on ACP). Consumes theTurnDriverinterface only; engine selection is per-call from config.driver-types.ts: Engine-agnosticTurnDrivercontract (request/handle/snapshot types). Everything above the drivers depends on this only.driver.ts: Stream-json driver (DEFAULT engine): one persistentagy --input-format stream-json --output-format stream-jsonprocess, turn serialization, recycle on profile drift, conversation binding, idle/abort timers.daily-log.ts: Daily NDJSON support log (~/.pi/extensions-data/estebanforge/pi-antigravity-bridge/logs/<YYYY-MM-DD>.ndjson): error-only by default (zero routine disk writes; warns toast in the pi UI instead), full trail needsAGY_DEBUG, 14-day retention, secret redaction, never throws. Fed by both drivers, the bridge, round-trips,/agy, andask-tool.redact.ts:redactText(): pattern-based secret scrubbing for free text (API-key prefixes, OAuth tokens, credential headers, token-ish JSON fields), word-boundary anchored, no internal length cap. Applied at reassembled-stderr boundaries (drivers, ACP connection, web tools, delegation exit note) and by daily-log value scanning; tokens split across pipe chunks still match because redaction sees the reassembled buffer.acp/jsonrpc.ts: NDJSON JSON-RPC 2.0 framing: correlation map, typed error results, server-to-client requests, line buffering (partial stdio frames).acp/connection.ts: ACP server process + protocol: initialize, session/new+load (bridgemcpServerson both), prompt with image/resource blocks,set_config_option, cancel probing, parked permission answering (skip turns auto-allow; a wired human handler holds the JSON-RPC reply open under a 480s deny-by-default budget,*_alwaysanswers remembered per connection; every failure path fail-closes).acp/events.ts:session/update→ DriverActivity mapping (pure). Tool frames: args unwrapargumentsenvelope, name from_meta.mcp.tool,content[]text/diff extraction. Load-replay suppression lives in the connection.acp/driver.ts:AcpDriver: serialized turns, remaining-budget timer pause on parks, Gate D abort (cancel probe → -32601 → teardown+kill+reload), connection-scoped exit handling, engine-scoped snapshots (reconnects, agentInfo, cancelSupported).native-tools.ts: Stream-json only: maps agy read-only tool steps onto real pi builtins (read/ls/grep/find) for native re-execution. Not used on ACP turns (Gate C).skills.ts:activate_skillbridge exposing the pi Agent Skills catalog to agy, answered by the bridge directly.bridge-catalog.ts: The live bridge catalog: filters pi's tools pertools/listAND pertools/callby the active set,bridgeToolsmode, session-only/agy toolshides, and the bridge's internal tools (AskAntigravity, antigravity, activate_skill, bridge_poll_result, agy_web_search/agy_read_url). A call outside the fresh set is rejected - a cached MCP catalog is not authorization.web-tools.ts: Opt-in web tools for any provider's model (agy_web_search,agy_read_url, default off viaconfig.webTools/AGY_WEB_TOOLS//agy web): each call spawns a one-shot search-only agy agent (--mode plan,inheritMcp: false, per-call agent dir under~/.gemini/config/agents/pi-bridge-web-*, removed infinally), enforces a 120s deadline + 2MiB cap, tolerates the OK/SUCCESS status drift, requires an observed nativesearch_web/read_url_contentstep, and refuses any disallowed tool step;sweepStaleWebAgentsat registration prunes dirs whose pid died (marker-less dirs after a 24h grace).
approval-gate.ts: Shadow tool factory (bash/write/edit) for the approval gate. Marker calls (__agyGate) are approval round-trips: ticket-verified, never executed locally; non-marker calls delegate to factory twins of the real builtins. Also maps agy native tool names onto the shadow surface.approval-detect.ts: Third-party pi permission-extension detection (settings packages name-match + config markers) andapprovals.gateMode: autoresolution (shadow only when a gate extension exists).approval-hook.ts: Gate hooks staging into the session-private per-pid bridge dir (bridgeMcpConfigDir()/.agents/hooks.json, discovered via the extra--add-dironly this session's agy receives — issue #5 isolation), the legacy sweep for gate groups dead 1.6.x sessions left in shared workspace files, and the generated early-ack + poll hook script; staged timeout exceeds the park budget because hook timeouts soft-pass upstream.mcp-registration.ts: Registers/unregisters the bridge in~/.gemini/config/mcp_config.jsonfor the stream-json CLI (per-pid entries, atomic writes, stale sweep). Delegation isolation:setBridgeEntriesDisabledflips our entries off aroundAskAntigravityspawns;acquireBridgeSuppressionrefcounts overlapping delegations and records each delegator pid in a cross-process marker (suppression.json, liveness + 24h age pruned) so concurrent sessions and the marker-aware session-start heal (healBridgeSuppression) never re-enable entries while a live delegation is in flight; fresh registrations land disabled too.patch-cleanup.ts: Detects a leftover invokeTool patch from pre-1.3.0 installs;/agy patch-cleanuprestores the original files from backup.discovery.ts: Conversation-id binding for the AskAntigravity one-shot tool (snapshot/diff + pid fd-scan;agy -pnever prints the id). Scheduled for deletion in phase 4.ask-tool.ts: TheAskAntigravityone-shot delegation tool (model/thinking defaults). Stays onagy -puntil phase 4;mode: "plan"keeps that path permanently (ACP has no review-only mode). Suppresses the bridge's global entries for the WHOLE delegated run (agy watches the config and hot-reloads on file changes, so a mid-run re-enable pokes the live delegation; release fires only on process close/error).config.ts: Configuration defaults (engine, acp.bin, acp.usageEstimate, bridgeTools, askTool, webTools, digest, systemPrompt, skipPermissions), directory resolution, and environment parsing.diff-render.ts:stream-jsononly: git-sourced edit diffs into pi's thinking stream.formatInlineDiff(no git) renders ACP's native diffs. ACP edits arrive as diffs intool_call content[].mcp-server.ts: Internal bridge HTTP/MCP server lifecycle and capability gating;tools/callparks into the provider round-trip. Shared-secretx-bridge-token; handle exposestokenfor ACPmcpServersheaders. Logs aprogress-tokenprobe when a request carries_meta.progressToken.models.ts: Antigravity model catalog loading and background cache refreshing;toPiModel(entry, input)advertises text+image input only when the engine isacp.sessions.ts: Persisted mapping ofpisession IDs to agy conversation/session IDs. Engine-scoped KEYS (sid:<x>streaming,sid:<x>@acp) so engine switches never cross conversations.engine-picker.ts: First-run onboarding:shouldOfferEnginePicker(no config file + noAGY_ENGINE, fail-closed to false),showEnginePickeroverlay (SelectList + DynamicBorder, stream-json preselected),isAgyInstalled(PATH scan or explicitAGY_BINpath),savedEngineMessage/agyMissingMessagetoast copy,toEnginevalue narrowing.
extensions/:piextension entry point (index.ts): provider registration, dual-driver wiring, bridgemcpServersregistration with the token header, the/agycommand (engine, tools, web, bridge, mode, permissions, timeout, digest, system-prompt, ask, model, thinking, acp-bin, auth, auth-manual, doctor, patch-cleanup, clear), ACP-only native tool display events (entry renderer,pi-bridgeself-filter, pending-tools status), the liveACP ≈ in / outusage status, engine-gated global MCP registration with sweep/heal running on BOTH engines, first-run engine picker + missing-agywarning insession_start, ACP pick pipeline (download now + chained sign-in, shared with/agy engine's no-args modal).scripts/: Utility scripts for development (smoke-in-pi.sh,smoke-stream-json.mjs,smoke-acp.mjs,smoke-acp-bridge.mjs,smoke-acp-image.mjs,parity-live.mjs,probe-acp-phase2.mjs,test-provider.ts,test-extension.ts).tests/: Test suite run by Vitest. ACP suites:acp-jsonrpc(framing, frame ceiling, noise filter),acp-events(mapping incl. probe-frame regressions),acp-driver(fake server intests/helpers/fake-acp-server.mjs),acp-config(engine narrowing). Bridge:bridge-catalog(active-set/mode/hidden filtering, internal-tool exclusion). Web tools:web-tools(answer gate, stale-agent sweep, URL/query validation; executable fixturetests/helpers/fake-agy-web.mjs). Onboarding:engine-picker(first-run gate, option order, download/sign-in disclosure pins, agy binary detection).
COMMANDS
| Action | Command |
|---|---|
| Install | npm install |
| Test | npm test |
| Build (typecheck) | npm run build |
| In-pi smoke | npm run smoke:pi |
| Live stream-json smoke | AGY_LIVE=1 node --experimental-strip-types scripts/smoke-stream-json.mjs (spends quota) |
| Live ACP smokes (bridge e2e, image) | AGY_ACP_LIVE=1 AGY_ACP_BIN=~/.local/opt/agy-acp/current/agy_acp_server.par npx tsx scripts/smoke-acp-bridge.mjs / smoke-acp-image.mjs |
| Live parity (both engines) | AGY_ACP_LIVE=1 ... npx tsx scripts/parity-live.mjs (7 scenarios × 2 engines) |
CODING STANDARDS
- Language: TypeScript (Strict mode enabled,
noEmit: true, module resolutionbundler). - Module System: Native ES Modules (
"type": "module"inpackage.json,.jsextension imports). - Style: Functional & utility-oriented structure, explicit typing, early returns, native error propagation.
- Testing: Vitest (
vitest run). ACP shapes are pinned against captures inprobe-logs/(gitignored, local only).
WHERE TO LOOK
- Source: src/ & extensions/
- Tests: tests/
- Docs: README.md & docs/
- ACP plan + protocol truth: docs/ACP-ADOPTION-PLAN.md & docs/ACP-PROTOCOL-REFERENCE.md
NOTES
- Two engines: default
stream-json(persistentagyCLI process);acp(beta) opt-in viaconfig.engine/AGY_ENGINE//agy engine acp(requires restart). Both implementTurnDriver; the provider layer (G1 digest, G10 system prompt, G9 round-trips) is shared. - First-run onboarding: no
config.json+ noAGY_ENGINE+ TUI = the engine picker modal atsession_start(stream-json first, esc re-asks next start). An acp pick chains the server download (status bar + chat milestones) and Google sign-in immediately; the same pipeline serves/agy enginewith no args. With stream-json active, a missingagybinary warns on every process start (per-process flag) until installed. - ACP limitations (RC01): no usage fields (Gate B - client-side token estimates via
acp.usageEstimatebridge the gap until Google ships usage:estimatemode streams live per-delta estimates that are superseded only by a real PromptResponse.usage (the connection-levelexactUsageSeenflag; diagnostic usage-shaped frames NEVER disable estimates),directpasses through server per-delta counts when they exist; both engines are permanently maintained peers), nosession/cancel(Gate D teardown+reload abort), no review-only mode (modes are permission modes only; plan delegations keepagy -p --mode plan). - Native display events (ACP-only): agy's own tool steps render as display-only pi cards (started/completed/failed with path, colored diff lines, command line, or output; persistence bounded at 4000/12000/500 chars). Events carrying
mcpServer: "pi-bridge"are filtered out so bridge round-trips with genuine pi cards never double-render. Stream-json keeps its stronger native re-exec instead. - ACP-only features: pi image attachments ride as typed content blocks; the G1 digest ships as an
embeddedContextresource block; edit diffs fromtool_call content[]render in the thinking stream with no git subprocesses (native re-exec and wrapper replay are retired on ACP turns). - Sessions: engine-scoped keys (
sid:<x>streaming,sid:<x>@acp) — engine switches never cross conversations and rollback preserves both bindings. - Architecture: The MCP tool bridge runs with no pi patch: bridge calls park in the provider's round-trip store and re-enter pi as real
toolUseturns (native cards, permissions, hooks). The pre-1.3.0 spawn-and-poll SQLite engine was removed in 1.3.2 (issue #1). - Approval gate: agy native tool calls (
run_command,create_file,edit_file, ...) can be routed through a pi-side approval:.agents/hooks.json(staged in the session-private per-pid bridge add-dir, so ONLY this session's agy loads it) PreToolUse parks inPOST /approval, the provider emits a shadowbash/write/edittoolUse (reusing the G9 round-trip), pi's permission extensions gate it, the decision rides back to the hook. Opt-in:approvals.gateMode auto|shadow|dedicated|off(default auto = off until a third-party gate extension is detected),approvals.mode ask|allow|denyfallback (headless deny). Marker calls never execute locally;verifyTicketdenies forged markers. All paths deny fail-closed (timeout, unwired, close). Hook discovery and execution live-verified 2026-09-22 (stream-json probe, marker hook fired); the full park→shadow-tool→decision round-trip is not yet live-verified. ACP turns have native parity:session/request_permissionparks on a dialog (ctx.ui.selectunder the dialog lock) with the turn budget paused, honoring the sameapprovals.modesemantics (allow = synchronous auto, deny/headless/esc = fail-closed) with per-connection*_alwaysmemory. - Context Continuity: The G1 context digest is opt-in (
config.digest/AGY_DIGEST//agy digest on, default off): it changes every turn and defeats the server-side prompt cache. Delivery differs per engine (inline vs embeddedContext resource block).