@estebanforge/pi-antigravity-bridge 1.7.5 → 1.7.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 +7 -0
- package/package.json +1 -1
- package/src/ask-tool.ts +159 -17
- package/src/web-tools.ts +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.6] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Plan-mode delegations enforce read-only instead of promising it.** The old design withheld the skip flag and relied on a prompt asking nicely; the field disagreed once — a plan delegation with the flag re-injected through the environment still edited files, because a prompt guard is a request, not a wall. Plan-mode runs now stage a temporary agy agent under `~/.gemini/config/agents/pi-bridge-ask-*` whose `tools:` list carries only `view_file` and `run_command`: with no file-editing tool in the toolset the model cannot emit an edit call at all (probed on agy 1.2.12 with and without `--dangerously-skip-permissions` — the restricted toolset reports "none" for edits both ways). The staged agent's `commandExecutionPolicy: auto` is what keeps reviews useful: analysis commands (git, rg, test runners) run headless for every user, with no `permissions.allow` edits — the per-user-config approach was rejected as unshippable for a product. With the agent staged, the skip flag returns under the same `skipPermissions` knob (default on): its only real danger was auto-approving edit calls, and none exist to approve. The prompt guard switches to a mutation-forbidding text (read-only commands allowed), and the tool description now states the true residual plainly: file-editing tools are hard-blocked, arbitrary shell execution under the flag stays prompt-guarded alone. Staging failure degrades fail-closed — no agent, no flag, the stricter all-commands-forbidden guard, with an env-injected skip flag stripped from `AGY_EXTRA_ARGS` on that path only. Agent dirs are nonce-named, pid-marked, removed in the run's finally, and orphans swept at registration (the web-delegate sweep gained a prefix parameter so both doctrines share one implementation).
|
|
10
|
+
- **Headless plan prompts say what they cannot do.** `agy -p` cannot answer permission prompts, and in plan mode a shell-command attempt is soft-denied: the run ends cleanly with nothing on stdout and the reason only on stderr. Plan prompts now append an explicit constraint — no shell commands, answer from the material in the prompt, state exactly what is missing — converting the silent death into a visible missing-content request (probed: the previously-fatal prompt shape now answers "Missing information: ..." in about ten seconds). With the staged reviewer agent above, this guard yields to the mutation-forbidding variant; it remains the rule on the staging-failure fallback path.
|
|
11
|
+
|
|
5
12
|
## [1.7.5] - 2026-09-25
|
|
6
13
|
|
|
7
14
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.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/ask-tool.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// nothing (see its defer guard). This module is the single source of truth.
|
|
13
13
|
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
15
16
|
import * as fs from "node:fs";
|
|
16
17
|
import * as path from "node:path";
|
|
17
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -28,6 +29,7 @@ import { loadConfig, type AgyMode, type ThinkingTier } from "./config.js";
|
|
|
28
29
|
import { redactText } from "./redact.js";
|
|
29
30
|
import { acquireBridgeSuppression } from "./mcp-registration.js";
|
|
30
31
|
import { AGY_EFFORT_ORDER, spawnAgyModelsRaw, toAgyEffort } from "./models.js";
|
|
32
|
+
import { sweepStaleWebAgents, webAgentsRoot } from "./web-tools.js";
|
|
31
33
|
|
|
32
34
|
// --- Constants -------------------------------------------------------------
|
|
33
35
|
|
|
@@ -78,7 +80,7 @@ TWO MODES (you choose):
|
|
|
78
80
|
- **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact.
|
|
79
81
|
|
|
80
82
|
EXECUTION MODES (param: mode):
|
|
81
|
-
- **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs
|
|
83
|
+
- **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs execute in a temporary restricted agent whose toolset has NO file-editing tools (hard block); the reviewer may still execute arbitrary shell commands, kept read-only by the prompt guard alone. Inline the material to review - a plan run cannot fetch it.
|
|
82
84
|
- **accept-edits** (default): agy applies edits directly inside the workspace.
|
|
83
85
|
|
|
84
86
|
COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request compact digests instead of full file contents. Defaults on for plan, off for accept-edits.
|
|
@@ -270,6 +272,109 @@ function extraArgs(): string[] {
|
|
|
270
272
|
return raw ? raw.split(/\s+/).filter((s) => s.length > 0) : [];
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
// --- Prompt assembly -------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
/** Guard for plan runs backed by the restricted reviewer agent. Difference
|
|
278
|
+
* to PLAN_HEADLESS_GUARD: commands are the analysis capability here (the
|
|
279
|
+
* agent's commandExecutionPolicy auto-approves them headless, probed
|
|
280
|
+
* 2026-09-28), so the guard forbids FILE MUTATION - by tool or by shell -
|
|
281
|
+
* and keeps the answer-from-material discipline. */
|
|
282
|
+
export const AGENT_REVIEW_GUARD = [
|
|
283
|
+
"",
|
|
284
|
+
"--- Review constraints ---",
|
|
285
|
+
"- Read-only review. Do not create, modify, or delete any files, including through shell commands (no redirects, tee, rm, mv, git commit).",
|
|
286
|
+
"- Read-only commands (git log, git diff, git show, rg, ls, cat, test runners) are allowed for analysis.",
|
|
287
|
+
"- Work from the material provided in this prompt; if information you need is missing, state exactly what is missing in your answer.",
|
|
288
|
+
].join("\n");
|
|
289
|
+
|
|
290
|
+
/** Appended to every headless plan-mode prompt. `agy -p` cannot answer
|
|
291
|
+
* permission prompts: in plan mode a run_command attempt is soft-denied and
|
|
292
|
+
* the turn ends AT the denial (exit 0, empty stdout, notice only on stderr -
|
|
293
|
+
* the empty-output branch in execute). Probed 2026-09-28 on agy 1.2.12:
|
|
294
|
+
* allow rules ARE consulted (a verbatim allow-listed `git log` runs), but
|
|
295
|
+
* --sandbox does NOT relax the command gate, so the only input every user
|
|
296
|
+
* is guaranteed is the prompt itself. The guard steers the model to answer
|
|
297
|
+
* from that material. accept-edits runs keep their tools and never get it. */
|
|
298
|
+
export const PLAN_HEADLESS_GUARD = [
|
|
299
|
+
"",
|
|
300
|
+
"--- Headless session constraints ---",
|
|
301
|
+
"- Do not run shell commands. Command execution is denied in this session, and any attempt ends the session immediately with no answer.",
|
|
302
|
+
"- Work only from the material provided in this prompt.",
|
|
303
|
+
"- If information you need is missing, state exactly what is missing in your answer instead of trying to fetch it.",
|
|
304
|
+
].join("\n");
|
|
305
|
+
|
|
306
|
+
/** Assemble the prompt sent to agy: digest marker first (existing behavior),
|
|
307
|
+
* then the caller's prompt, then the plan-mode guard last - the position
|
|
308
|
+
* the model reads with the most recency. No user-config mutation: this is
|
|
309
|
+
* the only plan-mode lever the tool itself owns. */
|
|
310
|
+
export function buildFinalPrompt(
|
|
311
|
+
prompt: string,
|
|
312
|
+
mode: AgyMode,
|
|
313
|
+
digest: boolean,
|
|
314
|
+
/** True when the restricted reviewer agent is staged: its empty edit
|
|
315
|
+
* toolset is the hard block, so the prompt may ALLOW read-only commands. */
|
|
316
|
+
agentEnforced = false,
|
|
317
|
+
): string {
|
|
318
|
+
let out = digest ? `(Use compact digests, not full file contents.)\n${prompt}` : prompt;
|
|
319
|
+
if (mode === "plan") out += agentEnforced ? AGENT_REVIEW_GUARD : PLAN_HEADLESS_GUARD;
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Plan-mode reviewer agent ----------------------------------------------
|
|
324
|
+
|
|
325
|
+
/** Temp agent dirs share the ~/.gemini/config/agents discovery root with the
|
|
326
|
+
* web delegates; AGY_AGENTS_ROOT overrides for tests and sandboxes. */
|
|
327
|
+
export function askAgentsRoot(): string {
|
|
328
|
+
return process.env.AGY_AGENTS_ROOT ?? webAgentsRoot();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const ASK_AGENT_PREFIX = "pi-bridge-ask-";
|
|
332
|
+
const ASK_AGENT_TOOLS = ["view_file", "run_command"];
|
|
333
|
+
|
|
334
|
+
/** agent.md for the plan-mode reviewer. The tools list is the ENFORCEMENT:
|
|
335
|
+
* with no file-editing tool present the model cannot emit an edit call at
|
|
336
|
+
* all (probed 2026-09-28 on agy 1.2.12: the toolset reports "none" for
|
|
337
|
+
* edits, with and without the skip flag). commandExecutionPolicy auto is
|
|
338
|
+
* what lets read commands run headless with no user allow rules. Never add
|
|
339
|
+
* a write-capable tool here. */
|
|
340
|
+
export function reviewerAgentMd(name: string): string {
|
|
341
|
+
return [
|
|
342
|
+
"---",
|
|
343
|
+
`name: ${name}`,
|
|
344
|
+
"description: Temporary Pi plan-mode reviewer",
|
|
345
|
+
"mainAgent: true",
|
|
346
|
+
"subagent: false",
|
|
347
|
+
"model: inherit",
|
|
348
|
+
"excludeDefaultComponents: true",
|
|
349
|
+
"inheritCustomizations: false",
|
|
350
|
+
"inheritMcp: false",
|
|
351
|
+
"commandExecutionPolicy: auto",
|
|
352
|
+
"tools:",
|
|
353
|
+
...ASK_AGENT_TOOLS.map((t) => ` - ${t}`),
|
|
354
|
+
"skills: []",
|
|
355
|
+
"rules: []",
|
|
356
|
+
"agents: []",
|
|
357
|
+
"mcpServers: []",
|
|
358
|
+
"---",
|
|
359
|
+
"",
|
|
360
|
+
"You are a strict read-only code reviewer. Review, analyze, and plan; never modify anything.",
|
|
361
|
+
"",
|
|
362
|
+
].join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Stage one unique reviewer agent (agent.md + pid marker) under root. Same
|
|
366
|
+
* hygiene doctrine as the web delegates: nonce-named, cleaned in finally,
|
|
367
|
+
* orphans swept by pid marker at registration. */
|
|
368
|
+
export function stageReviewerAgent(root: string): { name: string; dir: string } {
|
|
369
|
+
fs.mkdirSync(root, { recursive: true });
|
|
370
|
+
const name = `${ASK_AGENT_PREFIX}${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
371
|
+
const dir = path.join(root, name);
|
|
372
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
373
|
+
fs.writeFileSync(path.join(dir, "agent.md"), reviewerAgentMd(name), { mode: 0o600 });
|
|
374
|
+
fs.writeFileSync(path.join(dir, ".pid"), `${process.pid}\n`, { mode: 0o600 });
|
|
375
|
+
return { name, dir };
|
|
376
|
+
}
|
|
377
|
+
|
|
273
378
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
274
379
|
|
|
275
380
|
// --- Registration ----------------------------------------------------------
|
|
@@ -284,6 +389,13 @@ export async function registerAskAntigravityTool(
|
|
|
284
389
|
* verbose tier; info/warn/error always land. */
|
|
285
390
|
log?: (event: string, data?: unknown, level?: "debug" | "info" | "warn" | "error") => void,
|
|
286
391
|
): Promise<void> {
|
|
392
|
+
// Orphan sweep for the plan-mode reviewer agents (SIGKILL can skip the
|
|
393
|
+
// run's finally): same pid-marker doctrine as the web delegates.
|
|
394
|
+
try {
|
|
395
|
+
sweepStaleWebAgents(askAgentsRoot(), Date.now(), ASK_AGENT_PREFIX);
|
|
396
|
+
} catch {
|
|
397
|
+
// a sweep failure must never block registration
|
|
398
|
+
}
|
|
287
399
|
pi.registerTool({
|
|
288
400
|
name: "AskAntigravity",
|
|
289
401
|
label: "Ask Antigravity",
|
|
@@ -494,11 +606,27 @@ export async function registerAskAntigravityTool(
|
|
|
494
606
|
const snapshot = isContinuation ? null : snapshotConversations();
|
|
495
607
|
|
|
496
608
|
const mode: AgyMode = (params.mode as AgyMode | undefined) ?? "accept-edits";
|
|
609
|
+
// Plan runs stage a restricted reviewer agent: its tools list has NO
|
|
610
|
+
// file-editing tool, which is the hard edit block (the prompt guard
|
|
611
|
+
// alone was observed failing once - a sub-agent still edited files).
|
|
612
|
+
// Staging failure degrades to the legacy fallback: no agent, no skip
|
|
613
|
+
// flag, the stricter command-forbidding guard.
|
|
614
|
+
let reviewerAgent: { name: string; dir: string } | null = null;
|
|
615
|
+
if (mode === "plan") {
|
|
616
|
+
try {
|
|
617
|
+
reviewerAgent = stageReviewerAgent(askAgentsRoot());
|
|
618
|
+
} catch {
|
|
619
|
+
reviewerAgent = null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
497
622
|
const useDigest: boolean =
|
|
498
623
|
typeof params.digest === "boolean" ? params.digest : mode === "plan";
|
|
499
|
-
const finalPrompt: string =
|
|
500
|
-
|
|
501
|
-
|
|
624
|
+
const finalPrompt: string = buildFinalPrompt(
|
|
625
|
+
params.prompt,
|
|
626
|
+
mode,
|
|
627
|
+
useDigest,
|
|
628
|
+
reviewerAgent !== null,
|
|
629
|
+
);
|
|
502
630
|
|
|
503
631
|
// Opt-in full-context export (isolated stays the default).
|
|
504
632
|
let contextFile: string | null = null;
|
|
@@ -529,22 +657,31 @@ export async function registerAskAntigravityTool(
|
|
|
529
657
|
{ model: resolved.model, thinking: resolved.effort ?? config.defaultThinking, mode, digest: useDigest, continue: isContinuation, timeoutMin },
|
|
530
658
|
"info",
|
|
531
659
|
);
|
|
532
|
-
const
|
|
660
|
+
const extraRaw = extraArgs();
|
|
661
|
+
// Fail-closed on the fallback path: AGY_EXTRA_ARGS lands before the
|
|
662
|
+
// mode flags, so an env-injected skip flag would re-arm what the
|
|
663
|
+
// plan-mode rule strips. Only the enforced agent makes the flag safe.
|
|
664
|
+
const extra =
|
|
665
|
+
mode === "plan" && !reviewerAgent
|
|
666
|
+
? extraRaw.filter((a) => a !== "--dangerously-skip-permissions")
|
|
667
|
+
: extraRaw;
|
|
533
668
|
if (extra.length) args.push(...extra);
|
|
534
669
|
if (resolved.model) args.push("--model", resolved.model);
|
|
535
670
|
if (resolved.effort) args.push("--effort", resolved.effort);
|
|
536
671
|
args.push("--mode", mode);
|
|
537
|
-
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
|
|
672
|
+
if (reviewerAgent) args.push("--agent", reviewerAgent.name);
|
|
673
|
+
// Honor the shared permissions setting (same knob as the provider).
|
|
674
|
+
// accept-edits: the flag keeps commands from hanging on an
|
|
675
|
+
// unanswerable prompt in -p mode. Plan runs WITH the reviewer agent:
|
|
676
|
+
// the flag is safe again because the agent's toolset has no
|
|
677
|
+
// file-editing tool - the 2026-09-25 write incident rode edit tools
|
|
678
|
+
// that existed then; probed 2026-09-28 the restricted toolset reports
|
|
679
|
+
// "none" for edits even with the flag, and plan discipline turned a
|
|
680
|
+
// create-a-file request into a proposed plan instead of a write. The
|
|
681
|
+
// flag is what lets analysis commands (builds, test runners) run for
|
|
682
|
+
// users without allow rules. Plan fallback (staging failed): strict
|
|
683
|
+
// legacy behavior, no flag - the prompt guard is all that is left.
|
|
684
|
+
if (config.skipPermissions !== false && (mode !== "plan" || reviewerAgent !== null)) {
|
|
548
685
|
args.push("--dangerously-skip-permissions");
|
|
549
686
|
}
|
|
550
687
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
@@ -775,7 +912,7 @@ export async function registerAskAntigravityTool(
|
|
|
775
912
|
const note = [
|
|
776
913
|
"agy exited cleanly but produced no output.",
|
|
777
914
|
stderr ? `stderr: ${stderr}` : null,
|
|
778
|
-
"Common cause: a tool call needed a permission that headless mode cannot prompt for (typically the command gate in plan mode), so it was auto-denied
|
|
915
|
+
"Common cause: a tool call needed a permission that headless mode cannot prompt for (typically the command gate in plan mode), so it was auto-denied and the turn ended with no answer. Recovery: retry in plan mode with all needed content inlined in the prompt - plan runs cannot fetch it, commands are denied. Or rerun outside plan mode with skipPermissions, or add your own permissions.allow rules in ~/.gemini/antigravity-cli/settings.json.",
|
|
779
916
|
]
|
|
780
917
|
.filter(Boolean)
|
|
781
918
|
.join(" ");
|
|
@@ -803,6 +940,11 @@ export async function registerAskAntigravityTool(
|
|
|
803
940
|
fs.unlinkSync(contextFile);
|
|
804
941
|
} catch {}
|
|
805
942
|
}
|
|
943
|
+
if (reviewerAgent) {
|
|
944
|
+
try {
|
|
945
|
+
fs.rmSync(reviewerAgent.dir, { recursive: true, force: true });
|
|
946
|
+
} catch {}
|
|
947
|
+
}
|
|
806
948
|
}
|
|
807
949
|
},
|
|
808
950
|
});
|
package/src/web-tools.ts
CHANGED
|
@@ -37,10 +37,15 @@ export function webAgentsRoot(): string {
|
|
|
37
37
|
return path.join(os.homedir(), ".gemini", "config", "agents");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Remove leftover pi-bridge
|
|
41
|
-
* marker-less dirs only after a grace period (a live sibling
|
|
42
|
-
* between mkdir and its pid write). Never touches foreign
|
|
43
|
-
|
|
40
|
+
/** Remove leftover pi-bridge agent dirs matching `prefix`: dead-pid markers
|
|
41
|
+
* always, and marker-less dirs only after a grace period (a live sibling
|
|
42
|
+
* process may be between mkdir and its pid write). Never touches foreign
|
|
43
|
+
* agent dirs. The ask-tool reuses this for its reviewer agents. */
|
|
44
|
+
export function sweepStaleWebAgents(
|
|
45
|
+
root: string = webAgentsRoot(),
|
|
46
|
+
now = Date.now(),
|
|
47
|
+
prefix: string = WEB_AGENT_PREFIX,
|
|
48
|
+
): void {
|
|
44
49
|
let entries: string[];
|
|
45
50
|
try {
|
|
46
51
|
entries = readdirSync(root);
|
|
@@ -48,7 +53,7 @@ export function sweepStaleWebAgents(root: string = webAgentsRoot(), now = Date.n
|
|
|
48
53
|
return; // no agents dir yet: nothing to sweep
|
|
49
54
|
}
|
|
50
55
|
for (const entry of entries) {
|
|
51
|
-
if (!entry.startsWith(
|
|
56
|
+
if (!entry.startsWith(prefix)) continue;
|
|
52
57
|
try {
|
|
53
58
|
if (isStaleWebAgentDir(path.join(root, entry), now)) {
|
|
54
59
|
rmSync(path.join(root, entry), { recursive: true, force: true });
|