fix(gemini): handle workspace-trust gate from gemini-cli v0.39.1+ (#26)#29
fix(gemini): handle workspace-trust gate from gemini-cli v0.39.1+ (#26)#29
Conversation
… ADR-069) gemini-cli v0.39.1 introduced FatalUntrustedWorkspaceError that fires before any model call when invoked headlessly against a directory not marked trusted in interactive mode. Fresh installs of ask-gemini-mcp hit this with no actionable guidance, and the catch block would have triggered a useless Flash retry against the same untrusted directory. Set GEMINI_TRUST_WORKSPACE=true by default before spawn (forward- compatible env var; older Geminis silently ignore it). Detect trust errors in the catch block via FatalUntrustedWorkspaceError / "not running in a trusted directory" patterns, surface a friendly remediation message, and short-circuit the Flash retry. Opt-out via ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1 for users who want trust enforcement. Argv shape unchanged (env var, not flag) so all existing toEqual([...]) argv tests stay green. +6 tests in geminiExecutor.test.ts under a new "workspace trust handling" describe block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships the #26 workspace-trust fix to npm consumers. - packages/gemini-mcp: 1.6.0 -> 1.6.1 (carries the actual fix) - packages/llm-mcp: 0.3.1 -> 0.3.2 (orchestrator's prepack-bundle.mjs bundles gemini-mcp source at publish time, so the fix ships transitively to ask-llm-mcp users too) @ask-llm/plugin (also workspace-deps on gemini-mcp) is left at 0.3.1 — the plugin is distributed via marketplace on a separate cycle and was just bumped in f33b475. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @Lykhoyda's task in 2m 51s —— View job Re-review complete
All three findings from the first review are correctly applied. The implementation is solid. One non-blocking observation below. Previous findings — all addressed ✅
One non-blocking observation
Tracing the error path through
If gemini-cli emits a banner, a warning, or any preamble before the error line, The unit tests don't exercise this because they mock In practice this probably works: gemini-cli exception names are typically the first line of a stack trace. And even if A forward-safe guard would be to add Everything else looks good
This is ready to merge as-is. The |
| const reason = reasonMatch ? reasonMatch[1] : "rateLimitExceeded"; | ||
| Logger.error(`Gemini Quota Error: code=${status}, model=${model}, reason=${reason}`); | ||
| } | ||
| if (!trustLogged && WORKSPACE_TRUST_PATTERNS.some((pattern) => buffer.includes(pattern))) { |
There was a problem hiding this comment.
Minor semantic nit: Logger.error is used here for a detected-and-handled condition rather than an actual unrecoverable error. The stderr handler just logs for observability — consider Logger.warn instead, so the log level matches severity. The actual error surface is the friendly Error(ERROR_MESSAGES.WORKSPACE_TRUST_REQUIRED) thrown below in the catch block.
| if (!trustLogged && WORKSPACE_TRUST_PATTERNS.some((pattern) => buffer.includes(pattern))) { | |
| Logger.warn(STATUS_MESSAGES.WORKSPACE_TRUST_DETECTED); |
| } | ||
|
|
||
| function isWorkspaceTrustError(message: string): boolean { | ||
| return WORKSPACE_TRUST_PATTERNS.some((pattern) => message.includes(pattern)); |
There was a problem hiding this comment.
Case-sensitivity inconsistency: QUOTA_PATTERNS matching uses .toLowerCase() on both sides (line 519), but isWorkspaceTrustError uses exact-case includes. The patterns "FatalUntrustedWorkspaceError" and "not running in a trusted directory" are stable upstream identifiers, so this is likely fine in practice — but it's inconsistent with the defensive approach taken for quota errors. If you want to align the patterns, a simple normalisation here would do it:
| return WORKSPACE_TRUST_PATTERNS.some((pattern) => message.includes(pattern)); | |
| function isWorkspaceTrustError(message: string): boolean { | |
| const lower = message.toLowerCase(); | |
| return WORKSPACE_TRUST_PATTERNS.some((pattern) => lower.includes(pattern.toLowerCase())); | |
| } |
| expect(mockExecuteCommand).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it("does not flag quota errors as trust errors", async () => { |
There was a problem hiding this comment.
This test confirms quota errors still trigger Flash retry — good. One edge case not covered: ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1 set + gemini-cli fires FatalUntrustedWorkspaceError. With opt-out active, GEMINI_TRUST_WORKSPACE is never set, so the error fires naturally and should still be caught by isWorkspaceTrustError and throw the friendly message (step (a) of the remediation). A test like this would make the opt-out → trust-error flow explicit:
it("throws friendly error when ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1 and workspace is untrusted", async () => {
process.env.ASK_GEMINI_REQUIRE_WORKSPACE_TRUST = "1";
mockExecuteCommand.mockRejectedValueOnce(new Error("FatalUntrustedWorkspaceError: workspace not trusted"));
await expect(executeGeminiCLI({ prompt: "hello" })).rejects.toThrow(ERROR_MESSAGES.WORKSPACE_TRUST_REQUIRED);
expect(mockExecuteCommand).toHaveBeenCalledOnce();
});Not blocking — the two individual tests imply this already works — but it would document the intended UX for the opt-out path.
…itive trust check, opt-out test Three findings from claude[bot] code review (#29): 1. Logger.error -> Logger.warn for the stderr-detected trust condition. The condition is detected and handled (the actual error surface is the thrown ERROR_MESSAGES.WORKSPACE_TRUST_REQUIRED in the catch block); warn matches severity better than error. 2. isWorkspaceTrustError now matches case-insensitively, mirroring the defensive normalization already applied to QUOTA_PATTERNS detection. Patterns are stable upstream identifiers today, but issue #21 (multi- pattern quota detection) is precedent that upstream error formatting does drift; the cost of the .toLowerCase() pair on a cold path is zero. 3. Two new tests: - opt-out path (ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1) combined with a workspace-trust error from gemini, asserts friendly remediation message AND that GEMINI_TRUST_WORKSPACE is not set. - case-insensitivity regression test (FATALUNTRUSTEDWORKSPACEERROR uppercase) locks in finding #2 so a future contributor cannot "simplify" the matching back to exact-case without breaking the test. Test count: gemini-mcp 88 -> 90; project 422 -> 424. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review @claude. All three findings applied in
Test count: gemini-mcp 88 → 90; project 422 → 424. Ready for re-review. |
|
Claude finished @Lykhoyda's task in 1m 43s —— View job Re-review: all three findings applied correctly ✅
Finding 1 —
|
| Test | Covers |
|---|---|
Default sets GEMINI_TRUST_WORKSPACE=true |
happy-path env injection |
| Opt-out skips injection | ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1 guard |
| User-supplied value preserved | GEMINI_TRUST_WORKSPACE=false existing-value guard |
FatalUntrustedWorkspaceError → friendly error, no Flash retry |
error detection + short-circuit |
| "not running in a trusted directory" → friendly error | second pattern |
| Quota errors not misidentified as trust errors | non-regression |
| Opt-out + trust error → friendly message + no env var set | opt-out × error path |
FATALUNTRUSTEDWORKSPACEERROR all-caps → still matches |
case-insensitivity regression |
One minor wording note (non-blocking)
ERROR_MESSAGES.WORKSPACE_TRUST_REQUIRED (constants.ts:13-14) says "workspace-trust enforcement is enabled". This is accurate for the opt-out path (ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1 explicitly set) and for the GEMINI_TRUST_WORKSPACE=false explicit-override path, but would be mildly confusing in the hypothetical case where GEMINI_TRUST_WORKSPACE=true was set by the default and gemini-cli still fired the error (where fixes (a) and (b) wouldn't apply). That scenario shouldn't occur in practice — GEMINI_TRUST_WORKSPACE=true is the upstream-blessed bypass — so this is purely cosmetic and not worth changing.
Verdict
LGTM — ready to merge. All three review findings were applied as suggested, the test suite covers every code path, and the implementation is clean. No further changes needed.
Closes #26.
Summary
GEMINI_TRUST_WORKSPACE=trueby default before spawninggemini, restoring fresh-install parity against gemini-cli v0.39.1+ workspace-trust gate (upstream PR google-gemini/gemini-cli#25814)FatalUntrustedWorkspaceError/ "not running in a trusted directory" patterns and short-circuits the catch block with a friendly remediation message (no Flash retry — same dir would just fail again)ASK_GEMINI_REQUIRE_WORKSPACE_TRUST=1for users who want trust enforcement (e.g., shared CI runners)ask-gemini-mcp1.6.0 → 1.6.1 andask-llm-mcp0.3.1 → 0.3.2 (orchestrator'sprepack-bundle.mjsships gemini-mcp source transitively)This is action #1 only from the issue's §5 punch-list — the only item flagged "🔴 Hotfix required". Actions #2–#7 (stdin piping for >16 KiB prompts, Codex stabilized-hooks smoke test, sessionId/untrusted-workspace integration test,
--ignore-user-config/--ignore-ruleson Codex ephemeral path, Gemini tool-event progress forwarding, upstream version watch) are deferred to follow-up PRs as the issue itself recommended.Why env var, not
--skip-trustflagThe local Gemini at the time of authoring is v0.38.2, which has no
--skip-trustflag — adding that flag to argv would error on older Geminis with "unknown option."GEMINI_TRUST_WORKSPACE=trueis the upstream-blessed compatibility surface (gemini-cli's own E2E CI uses it) and unknown env vars are silently ignored on older versions, making it forward- and backward-compatible. Argv shape is unchanged, so every existingtoEqual([...])argv test stays green untouched. Full rationale in ADR-069 (docs/DECISIONS.md).Test plan
yarn test— 422 passing across 6 packages (was 414; +6 new trust-handling tests in gemini-mcp; package count 82 → 88)yarn lint— clean (Biome + tsc) across all 6 packagesyarn build— clean across the monorepo🤖 Generated with Claude Code