@estebanforge/pi-antigravity-bridge 1.7.9 → 1.7.10
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 +14 -0
- package/README.md +14 -3
- package/extensions/index.ts +65 -9
- package/package.json +1 -1
- package/src/ask-tool.ts +86 -37
- package/src/config.ts +61 -9
- package/src/driver.ts +10 -10
- package/src/provider.ts +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.10] - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`readOnly` config key (fail-closed provider kill switch).** `AGY_READONLY` env and `/agy readonly on|off` set it. On: every provider turn is refused with a visible error instead of running, because the agy CLI has no review-only enforcement on any path (probed 2026-10-07, [google-antigravity/antigravity-cli#1181](https://github.com/google-antigravity/antigravity-cli/issues/1181): plan mode stages a plan and executes the write in the same turn on both the `-p` and stream-json paths, with `permission_mode: "request-review"` reported and no permission event for the write; with `--disable-slash-commands` plan is a warned no-op). Built for tool-less callers (`pi -p --no-tools --model antigravity/...`) that must never mutate a workspace. While the knob is on, the `AskAntigravity` tool is refused as well; nothing in this package runs read-only, because the CLI cannot enforce it.
|
|
10
|
+
- **`AskAntigravity` mode default now follows the config.** Omitting `mode` used to hardcode `accept-edits`, so a `mode: "plan"` config still left the delegation tool write-capable by default (issue #3 request 3). The default is now plan when the configured mode is plan; an explicit `mode` param still wins.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Plan-shaped provider turns refuse visibly instead of running write-capable.** `/agy mode plan` on the stream-json engine was silently a no-op (the engine warns plan has no effect with slash expansion disabled), and the `-p` path is no better: probed 2026-10-07, plan stages a plan, asks for approval, and executes the write in the same turn on both paths ([#1181](https://github.com/google-antigravity/antigravity-cli/issues/1181)). Provider turns with `mode: "plan"` or `readOnly` on now fail with an error pointing at host-side enforcement and the knob to lift. The README no longer claims provider plan mode "genuinely executes nothing" or that the one-shot path enforces plan.
|
|
15
|
+
- **`readOnly` refusals cover the `AskAntigravity` tool too, and plan runs never carry the skip flag.** `AskAntigravity` is refused while `readOnly` is on (a `mode` param cannot bypass the kill switch), and the tool's plan runs no longer pass `--dangerously-skip-permissions` even when the reviewer agent stages: the CLI does not gate writes under plan mode ([#1181](https://github.com/google-antigravity/antigravity-cli/issues/1181)), so an auto-approved plan run is a write-capable run. Analysis commands in plan runs now fail visibly (headless auto-deny) instead of running approved; `accept-edits` runs keep the flag.
|
|
16
|
+
- **Delegation tool refuses `background: true` (fail-closed).** The param is advertised and always refused with an explanation: a wake from a nested delegation would land in the pi session, and long delegations already have the bridge's own early-ack plus poll flow. No behavior change for existing calls.
|
|
17
|
+
|
|
18
|
+
|
|
5
19
|
## [1.7.9] - 2026-10-06
|
|
6
20
|
|
|
7
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
135
135
|
/agy status summary, or open the full settings picker (TUI)
|
|
136
136
|
/agy doctor single status surface: bridge state, driver counters, versions, acp state, lifecycle, settings, log dir
|
|
137
137
|
/agy auth run the antigravity-acp sign-in now (engine acp): opens the Google login in your browser, shows the URL when no browser opens
|
|
138
|
-
/agy mode plan
|
|
138
|
+
/agy mode plan provider turns are REFUSED (the CLI cannot enforce review-only, #1181)
|
|
139
139
|
/agy mode accept-edits agy applies edits directly (default)
|
|
140
140
|
/agy permissions on|off auto-approve / prompt for tool calls (see warning)
|
|
141
141
|
/agy ask on|off register the AskAntigravity delegation tool (default on; off keeps only the provider and models, even with pi-ask-antigravity installed)
|
|
@@ -158,7 +158,13 @@ pi itself has no built-in approval gate. Unlike codex, claude, or agy running in
|
|
|
158
158
|
|
|
159
159
|
Because agy runs non-interactively under this provider (nothing can answer a `y/n` prompt), this extension passes `--dangerously-skip-permissions` by default on `accept-edits` runs. It is technically necessary there: `accept-edits` auto-approves file edits but not shell commands, so a `run_command` would otherwise hang forever waiting for a prompt nothing can answer (upstream [google-antigravity/antigravity-cli#318](https://github.com/google-antigravity/antigravity-cli/issues/318)). The net effect is that agy executes commands the same way pi already executes your other tools: without per-action review.
|
|
160
160
|
|
|
161
|
-
Plan mode never receives the flag: the flag auto-approves every permission request,
|
|
161
|
+
Plan mode never receives the flag: the flag auto-approves every permission request, so an auto-approved plan run is a write-capable run (the CLI does not gate writes under plan mode, see next paragraph). Do not combine `--sandbox` with skip-permissions ([#36](https://github.com/google-antigravity/antigravity-cli/issues/36)).
|
|
162
|
+
|
|
163
|
+
**Provider turns cannot run review-only.** Probed 2026-10-07 ([google-antigravity/antigravity-cli#1181](https://github.com/google-antigravity/antigravity-cli/issues/1181)): on every tested path (one-shot `-p` and the stream-json I/O format, with and without `--add-dir`), `--mode plan` stages a plan, asks for review and approval, and executes the requested file write in the same turn (`permission_mode: "request-review"` is reported; no permission event appears for the write). With `--disable-slash-commands` the CLI additionally warns that `--mode plan` has no effect. The `readOnly` knob below exists because of this: plan-shaped provider turns refuse visibly instead of pretending. `AskAntigravity` with `mode: "plan"` adds a staged restricted agent whose toolset omits file-editing tools (probed 2026-09-28 on agy 1.2.12; re-verify on 1.3.x), but plan mode itself does not gate writes anywhere. Treat host-side pi tool restrictions as the only hard read-only wall.
|
|
164
|
+
|
|
165
|
+
### readOnly
|
|
166
|
+
|
|
167
|
+
`"readOnly": true` in the config (or `AGY_READONLY=1`, or `/agy readonly on`) is a fail-closed kill switch: every provider turn AND the `AskAntigravity` tool are refused with a visible error instead of running write-capable under a read-only label. Built for callers like `pi -p --no-tools --model antigravity/...` health checks that must never mutate a workspace: with no review-only enforcement available in the CLI (see Permissions), a visible refusal is the only honest behavior. The knob covers provider turns and the ask tool; the web tools still spawn agy for searches. Default false.
|
|
162
168
|
|
|
163
169
|
For per-action review of agy's mutating native tools (`run_command`, `create_file`, `edit_file`, ...), see the [Approval gate](docs/APPROVAL-GATE.md): with it on, the call must pass a pi-side approval (your permission extension, or the built-in ask/allow/deny fallback) before agy executes it.
|
|
164
170
|
|
|
@@ -175,7 +181,8 @@ For isolation when running any agent that executes commands without a confirmati
|
|
|
175
181
|
| `AGY_ACP_BIN` | Path to the ACP server binary (`agy_acp_server.par`). Defaults to `agy_acp_server.par` on PATH. Wins over `config.acp.bin`. When neither points at a binary, auto-setup installs one. |
|
|
176
182
|
| `AGY_EXTRA_ARGS` | Extra args appended to every invocation. Whitespace-split. |
|
|
177
183
|
| `AGY_CONVERSATIONS_DIR` | Override the conversations DB directory. |
|
|
178
|
-
| `AGY_MODE` | Override execution mode: `plan` (
|
|
184
|
+
| `AGY_MODE` | Override execution mode: `plan` (provider turns are refused, see Permissions) or `accept-edits` (default). Wins over the config file. |
|
|
185
|
+
| `AGY_READONLY` | `1`/`true`/`on` (or any set value except `0`/`false`/`off`/empty) turns every provider turn and the `AskAntigravity` tool into a visible refusal (fail-closed: the CLI has no review-only enforcement on any path, [#1181](https://github.com/google-antigravity/antigravity-cli/issues/1181)). Wins over the config file. |
|
|
179
186
|
| `AGY_SKIP_PERMISSIONS` | `1`/`true` (default) to pass `--dangerously-skip-permissions` so commands don't hang on an unanswerable prompt in `-p` mode. `0`/`false` to prompt (hangs any `run_command` non-interactively). Wins over the config file. |
|
|
180
187
|
| `AGY_USAGE_ESTIMATE` | ACP token display: `estimate` (default; live client-side per-delta estimates), `direct` (pass through the server's own per-delta counts when it sends them), `off` (no usage display). Anything else falls back to `estimate`. Wins over the config file. |
|
|
181
188
|
| `AGY_DEFAULT_MODEL` | Default model alias for the `AskAntigravity` tool (`flash`/`pro`/`gemini`, or a tier/version qualifier). Wins over the config file. |
|
|
@@ -219,6 +226,10 @@ pi-antigravity-bridge practical risk is low, near zero. But not zero: the "in co
|
|
|
219
226
|
|
|
220
227
|
This is engineering analysis, not legal advice. Use against your own Antigravity account at your own risk; I am not responsible for any consequence to your account.
|
|
221
228
|
|
|
229
|
+
## Delegation tool: background is refused
|
|
230
|
+
|
|
231
|
+
The `AskAntigravity` delegation tool rejects `background: true` by design: a background wake would land in the pi session even when the caller is a nested delegation through the bridge catalog, and long delegations already have the bridge's own async flow (early-ack plus `bridge_poll_result`). Call without `background`; for unattended peer runs launch the CLI directly in a terminal.
|
|
232
|
+
|
|
222
233
|
## License
|
|
223
234
|
|
|
224
235
|
MIT.
|
package/extensions/index.ts
CHANGED
|
@@ -1053,6 +1053,7 @@ interface AgyCommandCtx {
|
|
|
1053
1053
|
|
|
1054
1054
|
interface PendingConfig {
|
|
1055
1055
|
mode?: AgyMode;
|
|
1056
|
+
readOnly?: boolean;
|
|
1056
1057
|
skipPermissions?: boolean;
|
|
1057
1058
|
defaultModel?: string;
|
|
1058
1059
|
defaultThinking?: ThinkingTier;
|
|
@@ -1074,6 +1075,10 @@ function settingsRows(ctx: AgyCommandCtx): string[] {
|
|
|
1074
1075
|
const row = (label: string, value: string) => ` ${label.padEnd(24)} ${value}`;
|
|
1075
1076
|
return [
|
|
1076
1077
|
row("mode:", config.mode),
|
|
1078
|
+
row(
|
|
1079
|
+
"readOnly:",
|
|
1080
|
+
config.readOnly ? "on (provider turns are refused)" : "off",
|
|
1081
|
+
),
|
|
1077
1082
|
row("permissions:", perm),
|
|
1078
1083
|
row("AskAntigravity tool:", config.askTool ? "on" : "off"),
|
|
1079
1084
|
row("Web tools:", config.webTools ? "on" : "off"),
|
|
@@ -1096,7 +1101,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
1096
1101
|
` sessions: ${ctx.store.size} bound`,
|
|
1097
1102
|
` config: ${CONFIG_PATH}`,
|
|
1098
1103
|
"",
|
|
1099
|
-
"Subcommands: /agy auth, /agy auth-manual, /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy agent <name|off>, /agy subagents, /agy quota, /agy artifacts [open <n|name>], /agy tasks [tail <id>], /agy bridge all|mcp|none, /agy tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy timeout <1-1440|off>, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear, /agy doctor",
|
|
1104
|
+
"Subcommands: /agy auth, /agy auth-manual, /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy readonly on|off, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy agent <name|off>, /agy subagents, /agy quota, /agy artifacts [open <n|name>], /agy tasks [tail <id>], /agy bridge all|mcp|none, /agy tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy timeout <1-1440|off>, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear, /agy doctor",
|
|
1100
1105
|
].join("\n");
|
|
1101
1106
|
}
|
|
1102
1107
|
|
|
@@ -1104,7 +1109,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
1104
1109
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
1105
1110
|
pi.registerCommand("agy", {
|
|
1106
1111
|
description:
|
|
1107
|
-
"Antigravity provider: doctor (health + settings), settings picker, clear sessions. Usage: /agy [doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|agent <name|off>|subagents|quota|artifacts [open <n|name>]|tasks [tail <id>]|bridge all|mcp|none|tools [hide|show <name>|reset]|web on|off|digest on|off|system-prompt on|off|timeout <1-1440|off>|acp-bin <path|auto>|patch-cleanup|clear]",
|
|
1112
|
+
"Antigravity provider: doctor (health + settings), settings picker, clear sessions. Usage: /agy [doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|readonly on|off|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|agent <name|off>|subagents|quota|artifacts [open <n|name>]|tasks [tail <id>]|bridge all|mcp|none|tools [hide|show <name>|reset]|web on|off|digest on|off|system-prompt on|off|timeout <1-1440|off>|acp-bin <path|auto>|patch-cleanup|clear]",
|
|
1108
1113
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
1109
1114
|
const ui = cmdCtx.ui;
|
|
1110
1115
|
if (ui) activeUi = ui;
|
|
@@ -1387,13 +1392,52 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1387
1392
|
if (sub === "mode") {
|
|
1388
1393
|
if (val === "plan" || val === "accept-edits") {
|
|
1389
1394
|
if (val === "plan" && ctx.engine === "acp") {
|
|
1390
|
-
ui?.notify("the ACP engine has no plan mode (RC01)
|
|
1395
|
+
ui?.notify("plan cannot be enforced on provider turns on either engine: the ACP engine has no plan mode (RC01), and the CLI does not gate writes under plan (google-antigravity/antigravity-cli#1181). /agy mode accept-edits.", "warning");
|
|
1391
1396
|
return;
|
|
1392
1397
|
}
|
|
1393
1398
|
const next = saveConfig({ mode: val as AgyMode });
|
|
1394
|
-
|
|
1399
|
+
// readOnly fail-closes provider turns regardless of this knob;
|
|
1400
|
+
// never let the confirmation imply turns will run. loadConfig, not
|
|
1401
|
+
// next: saveConfig merges the raw file, so an env-only readOnly
|
|
1402
|
+
// would otherwise read as off here.
|
|
1403
|
+
const effectiveReadonly = loadConfig().readOnly;
|
|
1404
|
+
if (effectiveReadonly) {
|
|
1405
|
+
ui?.notify(`mode set to ${next.mode}, but readOnly is on: provider turns are refused. /agy readonly off to lift.`, "info");
|
|
1406
|
+
} else if (val === "plan") {
|
|
1407
|
+
ui?.notify(`mode set to plan: provider turns are REFUSED (the CLI does not gate writes under plan, google-antigravity/antigravity-cli#1181). Only the AskAntigravity tool uses this mode.`, "info");
|
|
1408
|
+
} else {
|
|
1409
|
+
ui?.notify(`mode set to ${next.mode}`, "info");
|
|
1410
|
+
}
|
|
1411
|
+
} else {
|
|
1412
|
+
const cfg = loadConfig();
|
|
1413
|
+
ui?.notify(
|
|
1414
|
+
`current mode: ${cfg.mode}${cfg.readOnly ? " (readOnly on: provider turns are refused)" : ""}\nusage: /agy mode plan|accept-edits`,
|
|
1415
|
+
"info",
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (sub === "readonly") {
|
|
1421
|
+
if (val === "on" || val === "off") {
|
|
1422
|
+
const next = saveConfig({ readOnly: val === "on" });
|
|
1423
|
+
ui?.notify(
|
|
1424
|
+
next.readOnly
|
|
1425
|
+
? "readOnly is on: provider turns and AskAntigravity are REFUSED (the agy CLI has no review-only enforcement on any path; probed 2026-10-07). Enforce read-only in the host, then /agy readonly off."
|
|
1426
|
+
: "readOnly off: provider turns follow /agy mode again.",
|
|
1427
|
+
"info",
|
|
1428
|
+
);
|
|
1429
|
+
// Compare effective vs saved: the env can force either direction
|
|
1430
|
+
// (AGY_READONLY=1 keeps refusals on after /agy readonly off;
|
|
1431
|
+
// AGY_READONLY=0 silently keeps readOnly off after /agy readonly on).
|
|
1432
|
+
if (loadConfig().readOnly !== next.readOnly) {
|
|
1433
|
+
ui?.notify(`AGY_READONLY is set in this environment and overrides the config: readOnly stays ${loadConfig().readOnly ? "on (refusing)" : "off"} until you unset it.`, "warning");
|
|
1434
|
+
}
|
|
1395
1435
|
} else {
|
|
1396
|
-
|
|
1436
|
+
const cfg = loadConfig();
|
|
1437
|
+
ui?.notify(
|
|
1438
|
+
`readOnly is ${cfg.readOnly ? "on (provider turns and AskAntigravity are refused)" : "off"}\nusage: /agy readonly on|off. Fail-closed: the CLI has no review-only enforcement on any path (probed 2026-10-07), so on = refuse.`,
|
|
1439
|
+
"info",
|
|
1440
|
+
);
|
|
1397
1441
|
}
|
|
1398
1442
|
return;
|
|
1399
1443
|
}
|
|
@@ -1755,10 +1799,18 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1755
1799
|
id: "mode",
|
|
1756
1800
|
label: "Execution mode",
|
|
1757
1801
|
description:
|
|
1758
|
-
"accept-edits: agy applies edits directly. plan: review-only,
|
|
1802
|
+
"accept-edits: agy applies edits directly. plan: provider turns are REFUSED (the CLI cannot enforce review-only, upstream #1181); AskAntigravity plan runs add a restricted agent as a damper. Takes effect next turn.",
|
|
1759
1803
|
currentValue: config.mode,
|
|
1760
1804
|
values: ["accept-edits", "plan"],
|
|
1761
1805
|
},
|
|
1806
|
+
{
|
|
1807
|
+
id: "readonly",
|
|
1808
|
+
label: "Read-only provider turns",
|
|
1809
|
+
description:
|
|
1810
|
+
"on: provider turns are REFUSED (fail-closed; the CLI has no review-only enforcement on any path). Enforce read-only in the host instead.",
|
|
1811
|
+
currentValue: config.readOnly ? "on" : "off",
|
|
1812
|
+
values: ["off", "on"],
|
|
1813
|
+
},
|
|
1762
1814
|
{
|
|
1763
1815
|
id: "permissions",
|
|
1764
1816
|
label: "Permissions",
|
|
@@ -1845,6 +1897,8 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1845
1897
|
(id, newValue) => {
|
|
1846
1898
|
if (id === "mode") {
|
|
1847
1899
|
pending.mode = newValue as AgyMode;
|
|
1900
|
+
} else if (id === "readonly") {
|
|
1901
|
+
pending.readOnly = newValue === "on";
|
|
1848
1902
|
} else if (id === "permissions") {
|
|
1849
1903
|
pending.skipPermissions = newValue === "auto-approved";
|
|
1850
1904
|
} else if (id === "model") {
|
|
@@ -1882,12 +1936,13 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1882
1936
|
if (Object.keys(pending).length === 0) return;
|
|
1883
1937
|
|
|
1884
1938
|
// The picker cannot switch engines (command-only: /agy engine), but the
|
|
1885
|
-
// mode row can still produce plan while the latched engine is acp.
|
|
1886
|
-
//
|
|
1939
|
+
// mode row can still produce plan while the latched engine is acp. Plan
|
|
1940
|
+
// cannot be enforced on provider turns on either engine (#1181 + RC01);
|
|
1941
|
+
// refuse the combination.
|
|
1887
1942
|
const nextMode = pending.mode ?? config.mode;
|
|
1888
1943
|
if (nextMode === "plan" && ctx.engine === "acp") {
|
|
1889
1944
|
ui.notify(
|
|
1890
|
-
"plan
|
|
1945
|
+
"plan cannot be enforced on provider turns on either engine (google-antigravity/antigravity-cli#1181; ACP additionally has no plan mode, RC01). /agy mode accept-edits.",
|
|
1891
1946
|
"warning",
|
|
1892
1947
|
);
|
|
1893
1948
|
return;
|
|
@@ -1897,6 +1952,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
1897
1952
|
const next = saveConfig(pending);
|
|
1898
1953
|
const changed = [
|
|
1899
1954
|
pending.mode ? `mode=${next.mode}` : null,
|
|
1955
|
+
pending.readOnly !== undefined ? `readOnly=${next.readOnly ? "on" : "off"}` : null,
|
|
1900
1956
|
pending.skipPermissions !== undefined
|
|
1901
1957
|
? `permissions=${next.skipPermissions ? "auto-approved" : "prompt"}`
|
|
1902
1958
|
: null,
|
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.10",
|
|
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/ask-tool.ts
CHANGED
|
@@ -83,8 +83,8 @@ TWO MODES (you choose):
|
|
|
83
83
|
- **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact.
|
|
84
84
|
|
|
85
85
|
EXECUTION MODES (param: mode):
|
|
86
|
-
- **plan**: agy
|
|
87
|
-
- **accept-edits
|
|
86
|
+
- **plan**: agy proposes a plan for review-shaped tasks. NOT a security boundary: the CLI does not gate writes under plan mode (upstream google-antigravity/antigravity-cli#1181, probed 2026-10-07: the write can execute in the same turn). The run additionally stages a temporary restricted agent whose toolset has no file-editing tools, as a damper, and the skip-permissions flag is never passed. Inline the material to review - a plan run cannot fetch it.
|
|
87
|
+
- **accept-edits**: agy applies edits directly inside the workspace. Default unless the configured mode is plan.
|
|
88
88
|
|
|
89
89
|
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.
|
|
90
90
|
|
|
@@ -328,15 +328,17 @@ function extraArgs(): string[] {
|
|
|
328
328
|
// --- Prompt assembly -------------------------------------------------------
|
|
329
329
|
|
|
330
330
|
/** Guard for plan runs backed by the restricted reviewer agent. Difference
|
|
331
|
-
* to PLAN_HEADLESS_GUARD: commands are the analysis
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
331
|
+
* to PLAN_HEADLESS_GUARD: read commands are the intended analysis path
|
|
332
|
+
* here, so the guard forbids FILE MUTATION - by tool or by shell - and
|
|
333
|
+
* keeps the answer-from-material discipline. The agent is a damper, not
|
|
334
|
+
* enforcement (upstream #1181); a headless command attempt can still be
|
|
335
|
+
* auto-denied, which ends the run, so the guard steers toward view_file
|
|
336
|
+
* and staged material first. */
|
|
335
337
|
export const AGENT_REVIEW_GUARD = [
|
|
336
338
|
"",
|
|
337
339
|
"--- Review constraints ---",
|
|
338
340
|
"- Read-only review. Do not create, modify, or delete any files, including through shell commands (no redirects, tee, rm, mv, git commit).",
|
|
339
|
-
"-
|
|
341
|
+
"- Prefer the staged material and view_file for reading. If you run a read-only command (git log, git diff, rg, ls) and it comes back denied, the run ends without an answer: state what you could not check instead of retrying commands.",
|
|
340
342
|
"- Work from the material provided in this prompt; if information you need is missing, state exactly what is missing in your answer.",
|
|
341
343
|
].join("\n");
|
|
342
344
|
|
|
@@ -365,11 +367,12 @@ export function buildFinalPrompt(
|
|
|
365
367
|
mode: AgyMode,
|
|
366
368
|
digest: boolean,
|
|
367
369
|
/** True when the restricted reviewer agent is staged: its empty edit
|
|
368
|
-
* toolset is
|
|
369
|
-
|
|
370
|
+
* toolset is a damper (the CLI does not enforce review-only, upstream
|
|
371
|
+
* #1181), so the prompt may ALLOW read-only commands. */
|
|
372
|
+
agentDamper = false,
|
|
370
373
|
): string {
|
|
371
374
|
let out = digest ? `(Use compact digests, not full file contents.)\n${prompt}` : prompt;
|
|
372
|
-
if (mode === "plan") out +=
|
|
375
|
+
if (mode === "plan") out += agentDamper ? AGENT_REVIEW_GUARD : PLAN_HEADLESS_GUARD;
|
|
373
376
|
return out;
|
|
374
377
|
}
|
|
375
378
|
|
|
@@ -384,12 +387,14 @@ export function askAgentsRoot(): string {
|
|
|
384
387
|
const ASK_AGENT_PREFIX = "pi-bridge-ask-";
|
|
385
388
|
const ASK_AGENT_TOOLS = ["view_file", "run_command"];
|
|
386
389
|
|
|
387
|
-
/** agent.md for the plan-mode reviewer. The tools list is
|
|
388
|
-
* with no file-editing tool present the model
|
|
389
|
-
*
|
|
390
|
-
* edits
|
|
391
|
-
*
|
|
392
|
-
*
|
|
390
|
+
/** agent.md for the plan-mode reviewer. The tools list is a damper, not a
|
|
391
|
+
* guarantee: with no file-editing tool present the model has no edit tool in
|
|
392
|
+
* its menu (probed 2026-09-28 on agy 1.2.12: the toolset reports "none" for
|
|
393
|
+
* edits; re-verify on 1.3.x), but the CLI itself does not enforce
|
|
394
|
+
* review-only under plan mode (upstream google-antigravity/antigravity-cli
|
|
395
|
+
* #1181, probed 2026-10-07: the write can execute in the same turn).
|
|
396
|
+
* commandExecutionPolicy auto is what lets read commands run headless with
|
|
397
|
+
* no user allow rules. Never add a write-capable tool here. */
|
|
393
398
|
export function reviewerAgentMd(name: string): string {
|
|
394
399
|
return [
|
|
395
400
|
"---",
|
|
@@ -484,8 +489,7 @@ export async function registerAskAntigravityTool(
|
|
|
484
489
|
mode: Type.Optional(
|
|
485
490
|
Type.Union([Type.Literal("plan"), Type.Literal("accept-edits")], {
|
|
486
491
|
description:
|
|
487
|
-
"agy execution mode. 'plan' = review-only. 'accept-edits' = agy applies edits (
|
|
488
|
-
default: "accept-edits",
|
|
492
|
+
"agy execution mode. 'plan' = plan-shaped run: the CLI does not enforce review-only (see the tool description); the staged reviewer agent's toolset omits file-editing tools as a damper. 'accept-edits' = agy applies edits. Omit to follow the configured default (plan when config mode is plan, else accept-edits).",
|
|
489
493
|
}),
|
|
490
494
|
),
|
|
491
495
|
digest: Type.Optional(
|
|
@@ -509,6 +513,12 @@ export async function registerAskAntigravityTool(
|
|
|
509
513
|
"When true, export the current pi conversation (resolved, as markdown) to a temp file inside the workspace and tell agy to read it first. Default false (isolated one-shot). Opt in only when the user explicitly wants agy to see the full conversation; it costs agy tokens to read.",
|
|
510
514
|
}),
|
|
511
515
|
),
|
|
516
|
+
background: Type.Optional(
|
|
517
|
+
Type.Boolean({
|
|
518
|
+
description:
|
|
519
|
+
"NOT supported by this tool. The bridge runs agy through its own delegated flow and the answer must return synchronously to the caller. For background runs use the standalone @estebanforge/pi-ask-antigravity extension. Calls with background=true are refused.",
|
|
520
|
+
}),
|
|
521
|
+
),
|
|
512
522
|
}),
|
|
513
523
|
renderCall(args, theme, _context) {
|
|
514
524
|
// Show RESOLVED model/thinking/mode (config defaults applied) so the
|
|
@@ -524,7 +534,10 @@ export async function registerAskAntigravityTool(
|
|
|
524
534
|
thinkingArg ? toAgyEffort(thinkingArg as ThinkingLevel, AGY_EFFORT_ORDER) : undefined,
|
|
525
535
|
) ?? { model: requestedModel };
|
|
526
536
|
const thinking: ThinkingTier = resolved.effort ?? cfg.defaultThinking;
|
|
527
|
-
|
|
537
|
+
// Same default rule as execute: plan when the configured mode is
|
|
538
|
+
// plan, so the row always names what will actually run.
|
|
539
|
+
const mode: AgyMode =
|
|
540
|
+
(args.mode as AgyMode | undefined) ?? (cfg.mode === "plan" ? "plan" : "accept-edits");
|
|
528
541
|
const useDigest = typeof args.digest === "boolean" ? args.digest : mode === "plan";
|
|
529
542
|
const isContinue =
|
|
530
543
|
typeof args.conversationId === "string" && CONV_ID_RE.test(args.conversationId);
|
|
@@ -598,6 +611,37 @@ export async function registerAskAntigravityTool(
|
|
|
598
611
|
}
|
|
599
612
|
|
|
600
613
|
const config = loadConfig();
|
|
614
|
+
// readOnly refusal (fail-closed): the kill switch covers this tool too.
|
|
615
|
+
// It must not be bypassable with an explicit mode param: the CLI has no
|
|
616
|
+
// review-only enforcement (upstream google-antigravity/antigravity-cli
|
|
617
|
+
// #1181), so a plan-labeled run here is not read-only.
|
|
618
|
+
if (config.readOnly) {
|
|
619
|
+
return {
|
|
620
|
+
content: [
|
|
621
|
+
{
|
|
622
|
+
type: "text",
|
|
623
|
+
text: 'readOnly is on: AskAntigravity is refused (fail-closed; the agy CLI has no review-only enforcement, google-antigravity/antigravity-cli#1181). Run /agy readonly off to use this tool.',
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
details: emptyDetails(),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
// Background refusal (fail-closed): a wake from this tool would land in
|
|
630
|
+
// the pi session even when the CALLER is agy (nested delegation via the
|
|
631
|
+
// bridge catalog), and the bridge already owns an async delegation flow
|
|
632
|
+
// (early-ack + bridge_poll_result). The standalone pi-ask-antigravity
|
|
633
|
+
// extension is the background-capable tool.
|
|
634
|
+
if (params.background) {
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: 'background is not supported by the bridge\'s AskAntigravity: the answer must return synchronously to the caller. Long delegations are already covered by the bridge\'s own async flow (early-ack + bridge_poll_result); for an unattended peer run, launch agy directly in a terminal instead. Call again without background.',
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
details: emptyDetails(),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
601
645
|
const requestedModel = (params.model as string | undefined) ?? config.defaultModel;
|
|
602
646
|
if (typeof params.model === "string" && params.model.trim().startsWith("-")) {
|
|
603
647
|
return {
|
|
@@ -658,10 +702,13 @@ export async function registerAskAntigravityTool(
|
|
|
658
702
|
typeof rawConvId === "string" && rawConvId.length > 0 && CONV_ID_RE.test(rawConvId);
|
|
659
703
|
const snapshot = isContinuation ? null : snapshotConversations();
|
|
660
704
|
|
|
661
|
-
const mode: AgyMode =
|
|
705
|
+
const mode: AgyMode =
|
|
706
|
+
(params.mode as AgyMode | undefined) ?? (config.mode === "plan" ? "plan" : "accept-edits");
|
|
662
707
|
// Plan runs stage a restricted reviewer agent: its tools list has NO
|
|
663
|
-
// file-editing tool,
|
|
664
|
-
// alone was observed failing once - a sub-agent still edited
|
|
708
|
+
// file-editing tool, a toolset-level damper on edits (the prompt
|
|
709
|
+
// guard alone was observed failing once - a sub-agent still edited
|
|
710
|
+
// files; the CLI itself does not enforce review-only, upstream
|
|
711
|
+
// #1181).
|
|
665
712
|
// Staging failure degrades to the legacy fallback: no agent, no skip
|
|
666
713
|
// flag, the stricter command-forbidding guard.
|
|
667
714
|
let reviewerAgent: { name: string; dir: string } | null = null;
|
|
@@ -711,12 +758,14 @@ export async function registerAskAntigravityTool(
|
|
|
711
758
|
"info",
|
|
712
759
|
);
|
|
713
760
|
const extraRaw = extraArgs();
|
|
714
|
-
// Fail-closed on
|
|
715
|
-
//
|
|
716
|
-
// plan
|
|
761
|
+
// Fail-closed on every plan run: AGY_EXTRA_ARGS lands before the mode
|
|
762
|
+
// flags, so an env-injected skip flag would re-arm exactly what the
|
|
763
|
+
// never-flag-on-plan rule withholds (upstream #1181: an auto-approved
|
|
764
|
+
// plan run is write-capable). The reviewer agent is a damper, not a
|
|
765
|
+
// license for the flag.
|
|
717
766
|
const extra =
|
|
718
|
-
mode === "plan"
|
|
719
|
-
? extraRaw.filter((a) => a
|
|
767
|
+
mode === "plan"
|
|
768
|
+
? extraRaw.filter((a) => !a.startsWith("--dangerously-skip-permissions"))
|
|
720
769
|
: extraRaw;
|
|
721
770
|
if (extra.length) args.push(...extra);
|
|
722
771
|
if (resolved.model) args.push("--model", resolved.model);
|
|
@@ -725,16 +774,16 @@ export async function registerAskAntigravityTool(
|
|
|
725
774
|
if (reviewerAgent) args.push("--agent", reviewerAgent.name);
|
|
726
775
|
// Honor the shared permissions setting (same knob as the provider).
|
|
727
776
|
// accept-edits: the flag keeps commands from hanging on an
|
|
728
|
-
// unanswerable prompt in -p mode. Plan runs
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
if (config.skipPermissions !== false &&
|
|
777
|
+
// unanswerable prompt in -p mode. Plan runs NEVER get the flag: it
|
|
778
|
+
// auto-approves every permission request, and the CLI does not gate
|
|
779
|
+
// writes under plan mode (upstream
|
|
780
|
+
// google-antigravity/antigravity-cli#1181, probed 2026-10-07), so an
|
|
781
|
+
// auto-approved plan run is a write-capable run. The reviewer agent's
|
|
782
|
+
// toolset is a damper, not a guarantee; the 2026-09-28 "safe with the
|
|
783
|
+
// restricted toolset" probe predates the #1181 evidence. Without the
|
|
784
|
+
// flag, command attempts fail visibly (headless auto-deny) instead of
|
|
785
|
+
// running approved.
|
|
786
|
+
if (config.skipPermissions !== false && mode !== "plan") {
|
|
738
787
|
args.push("--dangerously-skip-permissions");
|
|
739
788
|
}
|
|
740
789
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
package/src/config.ts
CHANGED
|
@@ -89,11 +89,24 @@ export interface AgyConfig {
|
|
|
89
89
|
/** Official-server ACP engine options (used when engine = "acp"). */
|
|
90
90
|
acp: AcpConfig;
|
|
91
91
|
mode: AgyMode;
|
|
92
|
+
/** Fail-closed kill switch for provider turns. ON = every provider turn
|
|
93
|
+
* is refused visibly: the agy CLI has no review-only enforcement on any
|
|
94
|
+
* path (probed 2026-10-07, upstream #1181: plan mode stages a plan and
|
|
95
|
+
* executes the write in the same turn on -p and stream-json alike; the
|
|
96
|
+
* ACP engine has no plan mode at all). Built for callers like
|
|
97
|
+
* `pi -p --no-tools` health checks that must never mutate a workspace
|
|
98
|
+
* and prefer a visible refusal over a write-capable turn. Wins over
|
|
99
|
+
* `mode`; the AskAntigravity delegation tool is refused while this is on
|
|
100
|
+
* (the CLI has no review-only enforcement, so nothing here can run
|
|
101
|
+
* read-only). Env AGY_READONLY wins over the file.
|
|
102
|
+
* Default false. */
|
|
103
|
+
readOnly: boolean;
|
|
92
104
|
/** Auto-approve all agy tool permission requests (--dangerously-skip-permissions).
|
|
93
105
|
* Required for non-interactive use: without it, any `run_command` triggers an
|
|
94
106
|
* interactive y/n prompt that hangs forever in `-p` mode. Defaults true.
|
|
95
107
|
* DANGEROUS: lets agy run arbitrary commands (including destructive ones)
|
|
96
|
-
* without review.
|
|
108
|
+
* without review. The ask tool withholds the flag on plan runs;
|
|
109
|
+
* accept-edits runs need it to avoid hanging on the unanswerable prompt. */
|
|
97
110
|
skipPermissions: boolean;
|
|
98
111
|
/** AskAntigravity tool: default model alias (flash/pro/gemini or exact). */
|
|
99
112
|
defaultModel: string;
|
|
@@ -176,6 +189,7 @@ export interface AgyConfig {
|
|
|
176
189
|
const DEFAULTS: AgyConfig = {
|
|
177
190
|
engine: "stream-json",
|
|
178
191
|
mode: "accept-edits",
|
|
192
|
+
readOnly: false,
|
|
179
193
|
skipPermissions: true,
|
|
180
194
|
defaultModel: "flash",
|
|
181
195
|
defaultThinking: "medium",
|
|
@@ -212,18 +226,24 @@ export function parseCapMinutes(raw: string | number | undefined, fallback: numb
|
|
|
212
226
|
return n >= 1 && n <= MAX_TURN_CAP_MIN ? n : fallback;
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
/** Read the raw config file (no defaults, no env). Shared by loadConfig and
|
|
230
|
+
* saveConfig so persisted writes can never bake env overrides into the file. */
|
|
231
|
+
function readConfigFile(configPath: string): Partial<AgyConfig> {
|
|
218
232
|
try {
|
|
219
233
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
220
234
|
const parsed = JSON.parse(raw);
|
|
221
235
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
222
|
-
|
|
236
|
+
return parsed as Partial<AgyConfig>;
|
|
223
237
|
}
|
|
224
238
|
} catch {
|
|
225
|
-
/* missing or corrupt - fall back to
|
|
239
|
+
/* missing or corrupt - fall back to empty */
|
|
226
240
|
}
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Load config merged over defaults. Env vars override the file when set. */
|
|
245
|
+
export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
246
|
+
let file: Partial<AgyConfig> = readConfigFile(configPath);
|
|
227
247
|
|
|
228
248
|
// Env overrides file (matches the skipPermissions pattern).
|
|
229
249
|
// The naive OR `env === "plan" || file.mode === "plan"` would ignore an
|
|
@@ -249,6 +269,17 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
249
269
|
? envPerm === "1" || envPerm.toLowerCase() === "true"
|
|
250
270
|
: file.skipPermissions ?? DEFAULTS.skipPermissions;
|
|
251
271
|
|
|
272
|
+
const envReadOnly = process.env.AGY_READONLY;
|
|
273
|
+
// Fail-closed parse for a safety knob: empty string means the user
|
|
274
|
+
// explicitly blanked it (off), but ANY other set value that is not a
|
|
275
|
+
// spelled-out falsity counts as ON. " 1", "yes", typos: all refuse.
|
|
276
|
+
const readOnly =
|
|
277
|
+
envReadOnly !== undefined
|
|
278
|
+
? envReadOnly === ""
|
|
279
|
+
? false
|
|
280
|
+
: !["0", "false", "off"].includes(envReadOnly.trim().toLowerCase())
|
|
281
|
+
: file.readOnly ?? DEFAULTS.readOnly;
|
|
282
|
+
|
|
252
283
|
const defaultModelRaw =
|
|
253
284
|
process.env.AGY_DEFAULT_MODEL ?? file.defaultModel ?? DEFAULTS.defaultModel;
|
|
254
285
|
const defaultModel =
|
|
@@ -329,6 +360,7 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
329
360
|
engine,
|
|
330
361
|
acp,
|
|
331
362
|
mode,
|
|
363
|
+
readOnly,
|
|
332
364
|
skipPermissions,
|
|
333
365
|
defaultModel,
|
|
334
366
|
defaultThinking,
|
|
@@ -345,10 +377,30 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
345
377
|
};
|
|
346
378
|
}
|
|
347
379
|
|
|
348
|
-
/** Atomically persist a config patch (temp + rename).
|
|
380
|
+
/** Atomically persist a config patch (temp + rename). Merges onto the RAW
|
|
381
|
+
* FILE values, never loadConfig(): env overrides must not be baked into the
|
|
382
|
+
* file (AGY_READONLY=1 in the env would otherwise persist readOnly: true to
|
|
383
|
+
* disk on any unrelated save, outliving the env var). Nested objects are
|
|
384
|
+
* re-normalized to their known keys: a raw passthrough would resurrect
|
|
385
|
+
* legacy keys (acp.permissions) that loadConfig normalization drops. */
|
|
349
386
|
export function saveConfig(patch: Partial<AgyConfig>, configPath: string = CONFIG_PATH): AgyConfig {
|
|
350
|
-
const
|
|
351
|
-
|
|
387
|
+
const file = readConfigFile(configPath);
|
|
388
|
+
// Null-safe on nested objects: a hand-edited or legacy file can carry
|
|
389
|
+
// "acp": null; loadConfig tolerates it, so must saveConfig. Key presence
|
|
390
|
+
// triggers normalization (a null becomes the default shape, never null).
|
|
391
|
+
const fileAcp: Partial<AcpConfig> = file.acp && typeof file.acp === "object" ? file.acp : {};
|
|
392
|
+
const fileApprovals: Partial<GateConfig> = file.approvals && typeof file.approvals === "object" ? file.approvals : {};
|
|
393
|
+
const next: AgyConfig = {
|
|
394
|
+
...DEFAULTS,
|
|
395
|
+
...file,
|
|
396
|
+
...(file.acp !== undefined
|
|
397
|
+
? { acp: { bin: fileAcp.bin ?? DEFAULTS.acp.bin, usageEstimate: fileAcp.usageEstimate ?? DEFAULTS.acp.usageEstimate } }
|
|
398
|
+
: {}),
|
|
399
|
+
...(file.approvals !== undefined
|
|
400
|
+
? { approvals: { gateMode: fileApprovals.gateMode ?? DEFAULTS.approvals.gateMode, mode: fileApprovals.mode ?? DEFAULTS.approvals.mode } }
|
|
401
|
+
: {}),
|
|
402
|
+
...patch,
|
|
403
|
+
};
|
|
352
404
|
const dir = path.dirname(configPath);
|
|
353
405
|
fs.mkdirSync(dir, { recursive: true });
|
|
354
406
|
const tmp = `${configPath}.${process.pid}.tmp`;
|
package/src/driver.ts
CHANGED
|
@@ -80,10 +80,11 @@ interface ActiveTurn {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** The skip-permissions flag actually passed to agy. Plan mode never
|
|
83
|
-
* carries it: the flag auto-approves every permission request
|
|
84
|
-
*
|
|
85
|
-
* write
|
|
86
|
-
* must both go through this, or plan turns
|
|
83
|
+
* carries it: the flag auto-approves every permission request, and the CLI
|
|
84
|
+
* does not gate writes under plan mode (upstream #1181), so an
|
|
85
|
+
* auto-approved plan run is a write-capable run. #start (argv + profile)
|
|
86
|
+
* and #recycleCause (comparison) must both go through this, or plan turns
|
|
87
|
+
* recycle on every turn. */
|
|
87
88
|
function effectiveSkipPermissions(mode: DriverTurnRequest["mode"], skipPermissions: boolean): boolean {
|
|
88
89
|
return mode !== "plan" && skipPermissions;
|
|
89
90
|
}
|
|
@@ -352,12 +353,11 @@ export class StreamDriver implements TurnDriver {
|
|
|
352
353
|
this.#generation += 1;
|
|
353
354
|
const generation = this.#generation;
|
|
354
355
|
// Plan mode never carries the skip flag: the flag auto-approves ALL
|
|
355
|
-
// permission requests
|
|
356
|
-
//
|
|
357
|
-
// 2026-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
// recycle comparison stays consistent with the argv.
|
|
356
|
+
// permission requests, and the CLI does not gate writes under plan
|
|
357
|
+
// mode (upstream google-antigravity/antigravity-cli#1181, probed
|
|
358
|
+
// 2026-10-07: the write executes in the same turn). An auto-approved
|
|
359
|
+
// plan run is a write-capable run. The profile stores the EFFECTIVE
|
|
360
|
+
// value so the recycle comparison stays consistent with the argv.
|
|
361
361
|
const skipPermissions = effectiveSkipPermissions(request.mode, request.skipPermissions);
|
|
362
362
|
this.#profile = {
|
|
363
363
|
cwd: request.cwd,
|
package/src/provider.ts
CHANGED
|
@@ -1435,13 +1435,34 @@ export function createStreamSimple(
|
|
|
1435
1435
|
const config = loadConfig();
|
|
1436
1436
|
const engine = deps.engine ?? config.engine;
|
|
1437
1437
|
const selected = engine === "acp" && deps.acpDriver ? deps.acpDriver : deps.driver;
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1438
|
+
// Plan-shaped provider turns are fail-closed on BOTH engines. ACP has
|
|
1439
|
+
// no plan mode at all (RC01). Stream-json is worse and probed
|
|
1440
|
+
// 2026-10-07: the engine WARNS that --mode plan is a no-op while
|
|
1441
|
+
// --disable-slash-commands is set, executes file writes even with
|
|
1442
|
+
// slash expansion left on (it stages a plan.md and still writes in the
|
|
1443
|
+
// same turn), ignores --agent toolset restrictions, and file writes
|
|
1444
|
+
// bypass the permission system entirely. A plan-labeled turn here is
|
|
1445
|
+
// write-capable, so refuse visibly instead of running false safety.
|
|
1446
|
+
if (config.readOnly || config.mode === "plan") {
|
|
1441
1447
|
const partial = newAssistant(model);
|
|
1442
1448
|
const blocks: BlockState = { partial, textIdx: null, thinkingIdx: null, started: false };
|
|
1443
|
-
|
|
1444
|
-
|
|
1449
|
+
if (config.readOnly) {
|
|
1450
|
+
deps.log?.("turn-error", { reason: "readonly-refused" }, "warn");
|
|
1451
|
+
finalize(
|
|
1452
|
+
stream,
|
|
1453
|
+
blocks,
|
|
1454
|
+
"error",
|
|
1455
|
+
"readOnly cannot be enforced on provider turns: the agy CLI has no review-only enforcement on any path. Plan mode stages a plan and executes the write in the same turn (probed 2026-10-07, google-antigravity/antigravity-cli#1181). Enforce read-only in the host (pi tool restrictions or the approval gate), then /agy readonly off.",
|
|
1456
|
+
);
|
|
1457
|
+
return stream;
|
|
1458
|
+
}
|
|
1459
|
+
deps.log?.("turn-error", { reason: "plan-refused" }, "warn");
|
|
1460
|
+
finalize(
|
|
1461
|
+
stream,
|
|
1462
|
+
blocks,
|
|
1463
|
+
"error",
|
|
1464
|
+
"Plan mode cannot be enforced on provider turns: the agy CLI stages a plan and executes writes in the same turn (probed 2026-10-07, google-antigravity/antigravity-cli#1181), and the ACP engine has no plan mode (RC01). /agy mode accept-edits, and enforce review-only in the host.",
|
|
1465
|
+
);
|
|
1445
1466
|
return stream;
|
|
1446
1467
|
}
|
|
1447
1468
|
if (selected && roundTrips) {
|