Legacy Gemini hooks via read-only bind
Child C7 of epic 7425 (ACP runtime: every provider lane on ACP). Planned by Task 7428.
## Requirement (verbatim)
Epic goal: "... While a legacy path still exists, it keeps working."
Outcome 5 (A11): "Legacy Gemini still loads its per-dispatch hooks on gemini-cli 0.64.0 (a carrier the CLI trusts, as the ACP launch does)."
Current behaviour (PRD §3): Legacy `exec_line` exports `GEMINI_CLI_SYSTEM_SETTINGS_PATH` into a session-user-owned dir (`gemini_runner.py:109, 417-418, 633-634`), and `_fenced_exec` gets no `binds` (`:422`). The golden `tests/golden/gemini_launch.sh:14` pins it. The ACP launch already binds read-only (`:555-559`). Per the code comments and README:1046, gemini-cli 0.61/0.64 skips a system settings file whose parent is not root-owned.
## Approach (architecture §7, verbatim)
- In the legacy `GeminiAdapter.exec_line`, stop exporting `GEMINI_CLI_SYSTEM_SETTINGS_PATH` (`_SYSTEM_SETTINGS_ENV`, `gemini_runner.py:109, 417-418, 633-634`). Instead pass the rendered settings JSON to `_fenced_exec(..., binds='--perms 0444 --ro-bind-data 3 "$gemini_wt"/.gemini/settings.json')` with the content on fd 3. That is exactly the ACP launch's carrier (`:555-559`). **Reuse:** factor the "settings on fd 3, read-only bind" assembly into one helper that both the ACP and legacy launches call, rather than a second copy.
- Update `tests/golden/gemini_launch.sh`. Add a stand-in `gemini` test that prints the settings it loads, and assert the heartbeat and turn-end hook chains, the guards and `hooksConfig.enabled`. Existing quarantine tests keep `repo-gemini-dir-hidden`.
- **Risk:** the rendered settings file today also serves as the hook bridge's input (`render_hooks`). Check that no other reader depends on the `_meta` path before deleting it.
Seam (architecture §9): this child owns the settings-carrier helper in `GeminiAdapter.acp_launch` (`gemini_runner.py:517-574`): extract `_settings_bind(settings_json: str) -> tuple[str, str]`, returning the bwrap `binds` text and the fd-3 feed, and use it in both the ACP and legacy launches. C4 edits only the `control_mcp_server(..., env)` argument (:572) and C5 edits only :540-541 and the `AcpLaunch(...)` `authenticate=` keyword; do not touch those.
Credentials & reach:
- Replay, unit, API and golden tests; `check_provider_branches.py`; `./dc check full` — Nothing beyond the worktree — verified
- Dev control plane — `app.dev.jaah.ai` — verified: HTTP 200 and a UI sign-in from h5-3 (every dev oracle uses `app.dev.jaah.ai`)
- Dev console login (operator) — `jaah/debug/main/operator-login` — verified
- Gemini OAuth task (R8.4) — dev account `gem1` — **deferred until a healthy probe**: open the account's page, refresh its health and proceed only when healthy. If it stays unhealthy, ask once with the `Credential` header and settle the dev flow as unverified, named in the PR.
Out of scope: Do NOT un-hide the repo's own `.gemini/` (degradation `repo-gemini-dir-hidden` stays). Do NOT change the ACP launch's behaviour beyond calling the shared helper. Do NOT touch the control-server env (C4) or the API-key path (C5). Do NOT touch the chat ↔ terminal hand-off (Task 7370), registry and SDK v2 agents (Task 7429), billing rules, Settings › Providers layout. "No secret in any persisted surface". "No new provider-name branches (`tools/provider_branches.toml` allowlist growth)."
Done means:
- R8.1 The legacy Gemini launch delivers its per-dispatch settings as a read-only bind of the workspace `.gemini/settings.json` in the quarantine tmpfs (`_fenced_exec(binds=…)`), as the ACP launch does. It no longer exports `GEMINI_CLI_SYSTEM_SETTINGS_PATH` to a session-user-owned path. — verify: the golden `apps/jaahd/tests/golden/gemini_launch.sh` is updated, the test passes, and `grep GEMINI_CLI_SYSTEM_SETTINGS_PATH apps/jaahd/tests/golden/gemini_launch.sh` is empty; one helper serves both launches (no second copy of the bind assembly).
- R8.2 The CLI actually reads the hooks: a stand-in `gemini` that prints the settings it loads shows the heartbeat and turn-end hook chains, the harness guards and `hooksConfig.enabled`. — verify: a new test with the stand-in binary.
- R8.3 The repo's own `.gemini/` stays hidden (degradation `repo-gemini-dir-hidden` remains true). — verify: the existing quarantine tests pass.
- R8.4 (availability) — verify on dev (`app.dev.jaah.ai`): with the Gemini ACP switch OFF, run a Gemini OAuth task. On the task page, observe heartbeats arriving from the runner while it works, and the task settles. (deferred — `gem1` health; see Credentials & reach)
- L.1 Gemini OAuth with its switch OFF still runs legacy. — verify: legacy launch tests pass; golden scripts pass except the intended R8.1 change.
- D.1 `apps/jaahd/README.md` (~:1046) describes the present carrier for both launches. — verify: `python3 tools/check_context.py` passes.
- T.5 `python3 tools/check_provider_branches.py` passes, and no provider-name branch is added to its allowlist. — verify: run it and observe "OK"; `git diff tools/provider_branches.toml` adds no `[[allow]]`.
Required checks: ./dc check full
Outcome
Legacy Gemini now carries its settings as a read-only fd-3 workspace bind through the shared _settings_bind (no system-settings env). Also refuses an unopenable file and keeps the bind out of git. Quick gate green, PR #6051 merged; R8.4 dev check deferred.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)