@estebanforge/pi-ask-antigravity 1.2.8 → 1.2.9

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 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.2.9] - 2026-09-28
6
+
7
+ ### Fixed
8
+
9
+ - **Plan-mode delegations enforce read-only instead of promising it.** Port of the bridge fix one commit earlier in the stack: the prompt-only guard failed in the field (a plan delegation behind an env-injected skip flag still edited files), so plan-mode runs now stage a temporary agy agent — `tools:` limited to `view_file` and `run_command`, `commandExecutionPolicy: auto`, `inheritMcp: false` — under the shared `~/.gemini/config/agents` discovery root and pass `--agent`. No file-editing tool in the toolset means no edit call is possible, with or without `--dangerously-skip-permissions` (probed on agy 1.2.12); the policy is what lets analysis commands run headless for users with no allow rules. With the agent staged, the skip flag returns under the shared `skipPermissions` knob; staging failure falls back fail-closed to no agent, no flag, and the strict all-commands-forbidden guard, with an env-injected skip flag stripped from `AGY_EXTRA_ARGS` on that path. Includes the standalone sweep for orphaned agent dirs (pid-marker doctrine), duplicated rather than extracted: the standalone stays self-contained per repo convention. Tool descriptions state the residual honestly: file-editing tools are hard-blocked, arbitrary shell execution under the flag is prompt-guarded alone.
10
+ - **Headless plan prompts say what they cannot do.** Same hardening as the bridge: plan prompts append an explicit no-commands constraint and a state-what-is-missing instruction, converting the soft-denied empty-output death of a shell-command attempt into a visible missing-content request. On the enforced path the guard yields to the mutation-forbidding variant; the command-forbidding rule remains on the fallback path.
11
+
5
12
  ## [1.2.8] - 2026-09-25
6
13
 
7
14
  ### Fixed
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import { spawn } from "node:child_process";
32
+ import { randomBytes } from "node:crypto";
32
33
  import * as fs from "node:fs";
33
34
  import * as os from "node:os";
34
35
  import * as path from "node:path";
@@ -156,7 +157,7 @@ TWO MODES (you choose):
156
157
  - **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact — use for follow-ups, multi-turn refinement, or when the user says "ask agy to follow up / continue / now do X based on what you just did". Thread the id from each result into the next call.
157
158
 
158
159
  EXECUTION MODES (param: mode):
159
- - **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks. Enforced: plan runs never receive the skip-permissions flag, so a write or command attempt ends the run quickly with a "confirm plan" message instead of executing.
160
+ - **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.
160
161
  - **accept-edits** (default): agy applies edits directly inside the workspace.
161
162
  - For agy's orthogonal \`--sandbox\` shell-containment flag, set the \`AGY_EXTRA_ARGS=--sandbox\` env var.
162
163
 
@@ -282,6 +283,150 @@ function saveConfig(patch: Partial<Config>): SaveResult {
282
283
  return { path: targetPath, routedToProject: projectShadows };
283
284
  }
284
285
 
286
+ // --- Prompt assembly -------------------------------------------------------
287
+
288
+ /** Appended to every headless plan-mode prompt. `agy -p` cannot answer
289
+ * permission prompts: in plan mode a run_command attempt is soft-denied and
290
+ * the turn ends AT the denial (exit 0, empty stdout, notice only on stderr -
291
+ * the empty-output branch in execute). Probed 2026-09-28 on agy 1.2.12:
292
+ * allow rules ARE consulted (a verbatim allow-listed `git log` runs), but
293
+ * --sandbox does NOT relax the command gate, so the only input every user
294
+ * is guaranteed is the prompt itself. The guard steers the model to answer
295
+ * from that material. accept-edits runs keep their tools and never get it. */
296
+ export const PLAN_HEADLESS_GUARD = [
297
+ "",
298
+ "--- Headless session constraints ---",
299
+ "- Do not run shell commands. Command execution is denied in this session, and any attempt ends the session immediately with no answer.",
300
+ "- Work only from the material provided in this prompt.",
301
+ "- If information you need is missing, state exactly what is missing in your answer instead of trying to fetch it.",
302
+ ].join("\n");
303
+
304
+ /** Guard for plan runs backed by the restricted reviewer agent. Difference
305
+ * to PLAN_HEADLESS_GUARD: commands are the analysis capability here (the
306
+ * agent's commandExecutionPolicy auto-approves them headless, probed
307
+ * 2026-09-28), so the guard forbids FILE MUTATION - by tool or by shell -
308
+ * and keeps the answer-from-material discipline. */
309
+ export const AGENT_REVIEW_GUARD = [
310
+ "",
311
+ "--- Review constraints ---",
312
+ "- Read-only review. Do not create, modify, or delete any files, including through shell commands (no redirects, tee, rm, mv, git commit).",
313
+ "- Read-only commands (git log, git diff, git show, rg, ls, cat, test runners) are allowed for analysis.",
314
+ "- Work from the material provided in this prompt; if information you need is missing, state exactly what is missing in your answer.",
315
+ ].join("\n");
316
+
317
+ /** Assemble the prompt sent to agy: digest marker first (existing behavior),
318
+ * then the caller's prompt, then the plan-mode guard last - the position
319
+ * the model reads with the most recency. No user-config mutation: this is
320
+ * the only plan-mode lever the tool itself owns. */
321
+ export function buildFinalPrompt(
322
+ prompt: string,
323
+ mode: Mode,
324
+ digest: boolean,
325
+ /** True when the restricted reviewer agent is staged: its empty edit
326
+ * toolset is the hard block, so the prompt may ALLOW read-only commands. */
327
+ agentEnforced = false,
328
+ ): string {
329
+ let out = digest ? `(Use compact digests, not full file contents.)\n${prompt}` : prompt;
330
+ if (mode === "plan") out += agentEnforced ? AGENT_REVIEW_GUARD : PLAN_HEADLESS_GUARD;
331
+ return out;
332
+ }
333
+
334
+ // --- Plan-mode reviewer agent ----------------------------------------------
335
+
336
+ const ASK_AGENT_PREFIX = "pi-bridge-ask-";
337
+ const ASK_AGENT_TOOLS = ["view_file", "run_command"];
338
+
339
+ /** Temp agent dirs share the ~/.gemini/config/agents discovery root; the
340
+ * AGY_AGENTS_ROOT override exists for tests and sandboxes. */
341
+ export function askAgentsRoot(): string {
342
+ return process.env.AGY_AGENTS_ROOT ?? path.join(os.homedir(), ".gemini", "config", "agents");
343
+ }
344
+
345
+ /** agent.md for the plan-mode reviewer. The tools list is the ENFORCEMENT:
346
+ * with no file-editing tool present the model cannot emit an edit call at
347
+ * all (probed 2026-09-28 on agy 1.2.12: the toolset reports "none" for
348
+ * edits, with and without the skip flag). commandExecutionPolicy auto is
349
+ * what lets read commands run headless with no user allow rules. Never add
350
+ * a write-capable tool here. */
351
+ export function reviewerAgentMd(name: string): string {
352
+ return [
353
+ "---",
354
+ `name: ${name}`,
355
+ "description: Temporary Pi plan-mode reviewer",
356
+ "mainAgent: true",
357
+ "subagent: false",
358
+ "model: inherit",
359
+ "excludeDefaultComponents: true",
360
+ "inheritCustomizations: false",
361
+ "inheritMcp: false",
362
+ "commandExecutionPolicy: auto",
363
+ "tools:",
364
+ ...ASK_AGENT_TOOLS.map((t) => ` - ${t}`),
365
+ "skills: []",
366
+ "rules: []",
367
+ "agents: []",
368
+ "mcpServers: []",
369
+ "---",
370
+ "",
371
+ "You are a strict read-only code reviewer. Review, analyze, and plan; never modify anything.",
372
+ "",
373
+ ].join("\n");
374
+ }
375
+
376
+ /** Stage one unique reviewer agent (agent.md + pid marker) under root. Same
377
+ * hygiene doctrine as the web delegates: nonce-named, cleaned in finally,
378
+ * orphans swept by pid marker at registration. */
379
+ export function stageReviewerAgent(root: string): { name: string; dir: string } {
380
+ fs.mkdirSync(root, { recursive: true });
381
+ const name = `${ASK_AGENT_PREFIX}${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
382
+ const dir = path.join(root, name);
383
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
384
+ fs.writeFileSync(path.join(dir, "agent.md"), reviewerAgentMd(name), { mode: 0o600 });
385
+ fs.writeFileSync(path.join(dir, ".pid"), `${process.pid}\n`, { mode: 0o600 });
386
+ return { name, dir };
387
+ }
388
+
389
+ /** Remove leftover reviewer agent dirs: dead-pid markers always, marker-less
390
+ * dirs only after the grace period (a live sibling may sit between mkdir and
391
+ * its pid write). Never touches foreign agent dirs. */
392
+ export function sweepStaleAskAgents(
393
+ root: string = askAgentsRoot(),
394
+ now = Date.now(),
395
+ prefix: string = ASK_AGENT_PREFIX,
396
+ ): void {
397
+ let entries: string[];
398
+ try {
399
+ entries = fs.readdirSync(root);
400
+ } catch {
401
+ return; // no agents dir yet: nothing to sweep
402
+ }
403
+ for (const entry of entries) {
404
+ if (!entry.startsWith(prefix)) continue;
405
+ const dir = path.join(root, entry);
406
+ try {
407
+ let stale: boolean;
408
+ try {
409
+ const pid = Number.parseInt(fs.readFileSync(path.join(dir, ".pid"), "utf8").trim(), 10);
410
+ stale = Number.isInteger(pid) && pid > 0 ? !pidAlive(pid) : now - fs.statSync(dir).mtimeMs > 24 * 60 * 60 * 1000;
411
+ } catch {
412
+ stale = now - fs.statSync(dir).mtimeMs > 24 * 60 * 60 * 1000;
413
+ }
414
+ if (stale) fs.rmSync(dir, { recursive: true, force: true });
415
+ } catch {
416
+ // vanished mid-sweep: nothing to remove
417
+ }
418
+ }
419
+ }
420
+
421
+ function pidAlive(pid: number): boolean {
422
+ try {
423
+ process.kill(pid, 0);
424
+ return true;
425
+ } catch {
426
+ return false;
427
+ }
428
+ }
429
+
285
430
  // --- Model parsing + alias resolution --------------------------------------
286
431
 
287
432
  /** Parse one `agy models` line into a structured entry. */
@@ -776,6 +921,14 @@ export default async function (pi: ExtensionAPI) {
776
921
  // before (standalone tool).
777
922
  if (isBridgeInstalled()) return;
778
923
 
924
+ // Orphan sweep for the plan-mode reviewer agents (SIGKILL can skip the
925
+ // run's finally): same pid-marker doctrine as the bridge's web delegates.
926
+ try {
927
+ sweepStaleAskAgents();
928
+ } catch {
929
+ // a sweep failure must never block registration
930
+ }
931
+
779
932
  const binary = resolveAgy();
780
933
  // Discovered once at load; frozen for the session. Run /reload after an
781
934
  // `agy update` to refresh. Failure is non-fatal: resolveModel falls back
@@ -954,7 +1107,7 @@ export default async function (pi: ExtensionAPI) {
954
1107
  ],
955
1108
  {
956
1109
  description:
957
- "agy execution mode. 'plan' = review-only, no edits (--mode plan; the skip-permissions flag is never passed, so write and command attempts fail visibly at plan approval). 'accept-edits' = agy applies edits directly (--mode accept-edits, default). For agy's orthogonal --sandbox shell-containment flag, set the AGY_EXTRA_ARGS env var.",
1110
+ "agy execution mode. 'plan' = review-only (--mode plan in a temporary restricted agent: file-editing tools are hard-blocked; arbitrary shell commands remain prompt-guarded only; with skipPermissions on, commands still run for analysis). 'accept-edits' = agy applies edits directly (--mode accept-edits, default). For agy's orthogonal --sandbox shell-containment flag, set the AGY_EXTRA_ARGS env var.",
958
1111
  default: "accept-edits",
959
1112
  },
960
1113
  ),
@@ -1162,6 +1315,19 @@ export default async function (pi: ExtensionAPI) {
1162
1315
  const snapshot = isContinuation ? null : snapshotConversations(CONVERSATIONS_DIR);
1163
1316
 
1164
1317
  const mode: Mode = (params.mode as Mode | undefined) ?? "accept-edits";
1318
+ // Plan runs stage a restricted reviewer agent: its tools list has NO
1319
+ // file-editing tool, which is the hard edit block (the prompt guard
1320
+ // alone was observed failing once - a sub-agent still edited files).
1321
+ // Staging failure degrades to the legacy fallback: no agent, no skip
1322
+ // flag, the stricter command-forbidding guard.
1323
+ let reviewerAgent: { name: string; dir: string } | null = null;
1324
+ if (mode === "plan") {
1325
+ try {
1326
+ reviewerAgent = stageReviewerAgent(askAgentsRoot());
1327
+ } catch {
1328
+ reviewerAgent = null;
1329
+ }
1330
+ }
1165
1331
  // digest default: on for plan (review-only contexts where full file
1166
1332
  // contents are noise), off for accept-edits (agy applies edits and
1167
1333
  // may need richer context for diffs).
@@ -1169,9 +1335,12 @@ export default async function (pi: ExtensionAPI) {
1169
1335
  typeof params.digest === "boolean"
1170
1336
  ? params.digest
1171
1337
  : mode === "plan";
1172
- const finalPrompt: string = useDigest
1173
- ? `(Use compact digests, not full file contents.)\n${params.prompt}`
1174
- : params.prompt;
1338
+ const finalPrompt: string = buildFinalPrompt(
1339
+ params.prompt,
1340
+ mode,
1341
+ useDigest,
1342
+ reviewerAgent !== null,
1343
+ );
1175
1344
 
1176
1345
  // Opt-in full-context export (isolated stays the default).
1177
1346
  let contextFile: string | null = null;
@@ -1197,22 +1366,30 @@ export default async function (pi: ExtensionAPI) {
1197
1366
  : finalPrompt;
1198
1367
 
1199
1368
  const args: string[] = ["--add-dir", cwd];
1200
- const extra = extraArgs();
1369
+ const extraRaw = extraArgs();
1370
+ // Fail-closed on the fallback path: AGY_EXTRA_ARGS lands before the
1371
+ // mode flags, so an env-injected skip flag would re-arm what the
1372
+ // plan-mode rule strips. Only the enforced agent makes the flag safe.
1373
+ const extra =
1374
+ mode === "plan" && !reviewerAgent
1375
+ ? extraRaw.filter((a) => a !== "--dangerously-skip-permissions")
1376
+ : extraRaw;
1201
1377
  if (extra.length) args.push(...extra);
1202
1378
  if (resolved.model) args.push("--model", resolved.model);
1203
1379
  if (resolved.effort) args.push("--effort", resolved.effort);
1204
1380
  args.push("--mode", mode);
1381
+ if (reviewerAgent) args.push("--agent", reviewerAgent.name);
1205
1382
  // accept-edits auto-approves file edits but NOT shell commands, so a
1206
1383
  // run_command would hang on an unanswerable y/n prompt in non-interactive
1207
1384
  // -p mode. Honor the shared permissions setting (same knob as the bridge).
1208
- // Plan mode never gets the flag even when the knob is on: the flag
1209
- // auto-approves ALL permission requests including plan mode's own
1210
- // approval gate, which would silently turn "review-only" into full
1211
- // write access inside --add-dir (probed 2026-09-25: with the flag a
1212
- // plan run wrote files; without it, file and command attempts end
1213
- // exit 0 in ~20-30s with a "confirm plan" message - fail-visible,
1214
- // never a hang). Same fix as the bridge's ask-tool and driver.
1215
- if (mode !== "plan" && config.skipPermissions) {
1385
+ // Plan runs WITH the reviewer agent: the flag is safe again because the
1386
+ // agent's toolset has no file-editing tool - the 2026-09-25 write
1387
+ // incident rode edit tools that existed then; probed 2026-09-28 the
1388
+ // restricted toolset reports "none" for edits even with the flag. The
1389
+ // flag is what lets analysis commands (builds, test runners) run for
1390
+ // users without allow rules. Plan fallback (staging failed): strict
1391
+ // legacy behavior, no flag - the prompt guard is all that is left.
1392
+ if (config.skipPermissions && (mode !== "plan" || reviewerAgent !== null)) {
1216
1393
  args.push("--dangerously-skip-permissions");
1217
1394
  }
1218
1395
  if (isContinuation) args.push("--conversation", rawConvId as string);
@@ -1454,7 +1631,7 @@ export default async function (pi: ExtensionAPI) {
1454
1631
  const note = [
1455
1632
  "agy exited cleanly but produced no output.",
1456
1633
  details.stderr.trim() ? `stderr: ${details.stderr.trim()}` : null,
1457
- "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. Allow-list it under permissions.allow in ~/.gemini/antigravity-cli/settings.json, or rerun outside plan mode with skipPermissions.",
1634
+ "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.",
1458
1635
  ]
1459
1636
  .filter(Boolean)
1460
1637
  .join(" ");
@@ -1495,6 +1672,11 @@ export default async function (pi: ExtensionAPI) {
1495
1672
  fs.unlinkSync(contextFile);
1496
1673
  } catch {}
1497
1674
  }
1675
+ if (reviewerAgent) {
1676
+ try {
1677
+ fs.rmSync(reviewerAgent.dir, { recursive: true, force: true });
1678
+ } catch {}
1679
+ }
1498
1680
  }
1499
1681
  },
1500
1682
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-ask-antigravity",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Pi tool that delegates a self-contained sub-task to Google Antigravity's agy CLI (the CLI for Gemini). Friendly model aliases (flash/pro/gemini), configurable default model + thinking, and the AskAntigravity delegation tool.",
5
5
  "keywords": [
6
6
  "pi-package",