flake-update-cascade: never prompt on foreign nixConfig, keep Ctrl-C working #151

Merged
vnprc merged 1 commit from agent/cascade-noninteractive into master 2026-08-20 00:32:31 +01:00
Member

flake-update-cascade no longer freezes when a repo's flake declares its own binary caches (nixConfig.extra-substituters): the nix question that used to block the whole run on the TTY is auto-declined and the cascade moves on, and Ctrl-C now actually interrupts a stuck update instead of being swallowed.
The tool declines the foreign settings rather than adopting them; nothing changes for repos that declare no such settings — same commands, same 120-second bound, same commits and PRs — and declining is exactly what nix already did on its own whenever the cascade's output was piped.
Proof: a new regression test reproduces the freeze against the unfixed script (prompt shown, run killed at the bound) and shows the fixed script completing with the settings declined and the repo untouched; all existing cascade tests (20 assertions plus the multiple-inputs suite) still pass.
If this is wrong, revert the one commit: it touches only the three nix flake update invocations in one script and adds one test file; no data or lock formats change.

Mechanism

Two stacked defects, fixed identically at all three nix flake update sites:

  • Non-interactivity: each invocation now runs with stdin redirected from /dev/null. When nix wants to ask about untrusted flake configuration it reads stdin; with no stdin it declines and continues (warning: ignoring untrusted flake configuration setting 'extra-substituters') and the lock update still succeeds — verified on nix 2.31.5 with a fixture flake declaring extra-substituters and a local path input (exit 0, lock updated).
  • Interruptibility: timeout --foreground keeps nix in the terminal's foreground process group so SIGINT reaches it, and the 120s timeout can actually fire.

Rejected alternative: the issue suggested --accept-flake-config. It also silences the prompt (verified on nix 2.24.14: succeeds without prompting and updates the lock), but it does so by adopting the foreign flake's nix configuration — third-party substituter URLs included — into every cascade run. For lock resolution nothing is lost by declining (inputs are fetched from their declared sources; substituters serve realized store paths), and auto-trusting third-party binary-cache configuration is the wrong posture for this stack (architecture principle 4). --no-accept-flake-config was also tested and rejected: on prompting nix versions it still prompts, because explicit false and the default take the same code path.

Risk

  • Old-nix corner: on nix 2.24.14 (a stale VM's nix), a prompting repo under an interactive terminal now fails fast for that repo (error: unexpected EOF reading a line, exit 1) instead of hanging — the cascade's existing per-repo failure path reports it and continues with the other repos; without a tty, 2.24 declines cleanly and the update succeeds. Current nix 2.31.5 declines cleanly with a tty or without. No tested nix version hangs and none adopts foreign config.
  • timeout --foreground no longer isolates nix in its own process group, so when the 120s timeout fires it signals nix itself but not helper processes nix may have spawned; a fetch helper could briefly outlive it. That is the documented cost of making SIGINT work at all.
  • The new test needs real nix, util-linux script, and git on PATH (all present on stack VMs) and costs about 17s, 15s of which is the deliberate arm-1 wedge bound.

Validation

  • Reproduction (fixture flake with nixConfig.extra-substituters = [ "https://cache.example.invalid" ] and a local path input, no network needed): the unfixed timeout 120 nix flake update under a pty with stdin held open prints do you want to allow configuration setting 'extra-substituters' to be set to 'https://cache.example.invalid' (y/N)? and blocks until an outer bound kills it (exit 124), on both the system nix 2.31.5 and nix 2.24.14; on 2.31.5 an injected Ctrl-C is echoed (^C) and ignored.
  • Fixed invocation under the same pty conditions on nix 2.31.5: exits 0, prints the decline warning, and the lock update lands (the changed path input's narHash is picked up).
  • Interruptibility: with the prompt held under a pty on nix 2.31.5, timeout --foreground 120 nix flake update ... plus an injected Ctrl-C at 2s yields error: interrupted by the user immediately; plain timeout 120 ignores the same Ctrl-C until the outer bound kills the session.
  • New regression test tests/flake/flake-update-cascade-nixconfig.sh (real nix, real fixture repo, pty via script): arm 1 proves the fixture provokes the prompt on the running nix, so the test cannot rot into a vacuous pass; arm 2 runs the real cascade in --dry-run and asserts completion, the decline warning, and an unchanged repository lock. It passes against the fixed script (17s) and, run against the stashed unfixed script, fails as intended: FAIL: cascade did not complete (exit 124; 124 means it wedged at the prompt).
  • Existing suites, run against the fixed script in this branch: validation 3/3, preflight 6/6, dry-run 4/4, pr-mode 7/7, and flake-update-cascade-multiple-inputs.sh passed. No existing test needed changes: the mock nix never sees the stdin redirect and the real timeout consumes --foreground before the mock runs.
  • bash -n and shellcheck 0.11.0 are clean on the edited script; the test file passes bash -n.

Closes #143

`flake-update-cascade` no longer freezes when a repo's flake declares its own binary caches (`nixConfig.extra-substituters`): the nix question that used to block the whole run on the TTY is auto-declined and the cascade moves on, and Ctrl-C now actually interrupts a stuck update instead of being swallowed. The tool declines the foreign settings rather than adopting them; nothing changes for repos that declare no such settings — same commands, same 120-second bound, same commits and PRs — and declining is exactly what nix already did on its own whenever the cascade's output was piped. Proof: a new regression test reproduces the freeze against the unfixed script (prompt shown, run killed at the bound) and shows the fixed script completing with the settings declined and the repo untouched; all existing cascade tests (20 assertions plus the multiple-inputs suite) still pass. If this is wrong, revert the one commit: it touches only the three `nix flake update` invocations in one script and adds one test file; no data or lock formats change. ## Mechanism Two stacked defects, fixed identically at all three `nix flake update` sites: - Non-interactivity: each invocation now runs with stdin redirected from `/dev/null`. When nix wants to ask about untrusted flake configuration it reads stdin; with no stdin it declines and continues (`warning: ignoring untrusted flake configuration setting 'extra-substituters'`) and the lock update still succeeds — verified on nix 2.31.5 with a fixture flake declaring `extra-substituters` and a local path input (exit 0, lock updated). - Interruptibility: `timeout --foreground` keeps nix in the terminal's foreground process group so SIGINT reaches it, and the 120s timeout can actually fire. Rejected alternative: the issue suggested `--accept-flake-config`. It also silences the prompt (verified on nix 2.24.14: succeeds without prompting and updates the lock), but it does so by adopting the foreign flake's nix configuration — third-party substituter URLs included — into every cascade run. For lock resolution nothing is lost by declining (inputs are fetched from their declared sources; substituters serve realized store paths), and auto-trusting third-party binary-cache configuration is the wrong posture for this stack (architecture principle 4). `--no-accept-flake-config` was also tested and rejected: on prompting nix versions it still prompts, because explicit false and the default take the same code path. ## Risk - Old-nix corner: on nix 2.24.14 (a stale VM's nix), a prompting repo under an interactive terminal now fails fast for that repo (`error: unexpected EOF reading a line`, exit 1) instead of hanging — the cascade's existing per-repo failure path reports it and continues with the other repos; without a tty, 2.24 declines cleanly and the update succeeds. Current nix 2.31.5 declines cleanly with a tty or without. No tested nix version hangs and none adopts foreign config. - `timeout --foreground` no longer isolates nix in its own process group, so when the 120s timeout fires it signals nix itself but not helper processes nix may have spawned; a fetch helper could briefly outlive it. That is the documented cost of making SIGINT work at all. - The new test needs real `nix`, util-linux `script`, and `git` on PATH (all present on stack VMs) and costs about 17s, 15s of which is the deliberate arm-1 wedge bound. ## Validation - Reproduction (fixture flake with `nixConfig.extra-substituters = [ "https://cache.example.invalid" ]` and a local path input, no network needed): the unfixed `timeout 120 nix flake update` under a pty with stdin held open prints `do you want to allow configuration setting 'extra-substituters' to be set to 'https://cache.example.invalid' (y/N)?` and blocks until an outer bound kills it (exit 124), on both the system nix 2.31.5 and nix 2.24.14; on 2.31.5 an injected Ctrl-C is echoed (`^C`) and ignored. - Fixed invocation under the same pty conditions on nix 2.31.5: exits 0, prints the decline warning, and the lock update lands (the changed path input's narHash is picked up). - Interruptibility: with the prompt held under a pty on nix 2.31.5, `timeout --foreground 120 nix flake update ...` plus an injected Ctrl-C at 2s yields `error: interrupted by the user` immediately; plain `timeout 120` ignores the same Ctrl-C until the outer bound kills the session. - New regression test `tests/flake/flake-update-cascade-nixconfig.sh` (real nix, real fixture repo, pty via `script`): arm 1 proves the fixture provokes the prompt on the running nix, so the test cannot rot into a vacuous pass; arm 2 runs the real cascade in `--dry-run` and asserts completion, the decline warning, and an unchanged repository lock. It passes against the fixed script (17s) and, run against the stashed unfixed script, fails as intended: `FAIL: cascade did not complete (exit 124; 124 means it wedged at the prompt)`. - Existing suites, run against the fixed script in this branch: validation 3/3, preflight 6/6, dry-run 4/4, pr-mode 7/7, and flake-update-cascade-multiple-inputs.sh passed. No existing test needed changes: the mock nix never sees the stdin redirect and the real `timeout` consumes `--foreground` before the mock runs. - `bash -n` and shellcheck 0.11.0 are clean on the edited script; the test file passes `bash -n`. Closes #143
A repo whose flake declares nixConfig.extra-substituters made nix flake
update prompt on the TTY and the cascade wedged; because the update ran
under timeout in a separate process group, Ctrl-C never reached it. At
all three update sites, redirect stdin from /dev/null so nix
auto-declines foreign flake configuration instead of asking, and run
timeout with --foreground so terminal SIGINT and the timeout itself
reach nix.

Add a pty regression test: arm 1 proves the fixture still provokes the
prompt on the running nix, arm 2 pins the cascade completing with the
foreign config declined and the repository untouched.

Refs #143
vnprc approved these changes 2026-08-20 00:32:25 +01:00
vnprc merged commit 3b65aeb041 into master 2026-08-20 00:32:31 +01:00
vnprc deleted branch agent/cascade-noninteractive 2026-08-20 00:32:31 +01:00
Sign in to join this conversation.
No description provided.