feat(install): add --yes flag for non-interactive SDK installation (EAI-7956) - #273
Conversation
2481c80 to
f268078
Compare
b065fc1 to
792ff7f
Compare
|
Current head |
792ff7f to
0a2a239
Compare
|
Rebased onto current Conflict resolved (1 file):
Also fixed a rebase-induced numbering collision:
Re-ran on the rebased head (local, macOS):
The GPU/nightly install scenarios (Scenario 6/7, |
rominf
left a comment
There was a problem hiding this comment.
I read through this PR against the diff (didn't check anything out locally, just read the patch and the surrounding code).
The --yes flag mechanism itself looks solid: fresh installs never prompt, overwriting an existing managed SDK now goes through a clear approval gate (sdk_install_approval), the non-interactive refusal path bails before any download, and install_sdk returning SdkInstallResult { output, mutated } instead of keying finalization off dry_run is a genuinely better signal (the "user said no at the prompt" case correctly reports mutated: false and skips activation/engine auto-install). The new unit tests (existing_runtime_relation_*, sdk_install_approval_only_prompts_when_overwriting_existing) cover the interesting edge cases, including propagating a manifest-read error instead of silently treating it as "no existing runtime." The e2e scenarios match the stated intent and the call sites were all threaded through consistently.
One thing worth flagging before merge: this PR also quietly adds a second, unrelated feature. Alongside the --yes work, apps/rocm/src/therock.rs gained host_rocm_version_newer_than, repo_version_without_wheels, the newest_repo_version field on PipRuntimeResolution, and the version_note:/warning: lines they produce in both the wheel and tarball install paths — none of which have anything to do with non-interactive installation. This is a legacy-ROCm-vs-TheRock-version explanation feature, and it's not mentioned anywhere in the PR title, summary, or implementation notes, so a reviewer approving "the --yes flag PR" is also approving a second behavior change they weren't told about. It's also the one part of the diff with materially thinner testing: repo_version_without_wheels got a unit test, but host_rocm_version_newer_than (used at all four call sites) has none, and neither has any assertion (unit or e2e) on the actual version_note/warning output text. It also means every SDK install/dry-run now does an extra unconditional filesystem probe (detect_legacy_rocm_summary walking /opt, /usr/local, etc.) that has nothing to do with the stated change. I'd ask for this to be split into its own PR, or at minimum have the summary updated to describe it and get equivalent test coverage.
Everything else — the #[allow(clippy::too_many_arguments)] on the now 8-arg install_sdk, the prompt/confirmation code mirroring the existing confirm_uninstall pattern, the apply_runtime_update and dry-run call sites always passing assume_yes: true where that's appropriate (a fresh dry-run/update install never prompts) — looked fine on read-through.
| " latest_compatible_version: {}", | ||
| runtime_version_display(&resolution.latest_version) | ||
| ); | ||
| if let Some(host_version) = host_rocm_version_newer_than(&resolution.latest_version) { |
There was a problem hiding this comment.
This is where the bundled, undisclosed second feature starts (host_rocm_version_newer_than, and further down repo_version_without_wheels / newest_repo_version / the warning: line and the tarball-path duplicates at the equivalent spot in install_tarball_runtime). It explains a host's legacy ROCm version vs. the TheRock version being installed — unrelated to the --yes flag this PR is about, not mentioned in the PR description, and the only new logic in the diff without direct test coverage (repo_version_without_wheels has a unit test; host_rocm_version_newer_than itself doesn't, and no test asserts on the version_note:/warning: output text). Please split this into its own PR (with its own description and tests) or fold it into this one's summary so reviewers know it's in scope.
There was a problem hiding this comment.
Thanks — took the "disclose + cover it" option rather than splitting, since the version explanation and the --yes work touch the same install paths.
- Disclosed: the PR summary now has an "Also in this PR: host-ROCm-vs-TheRock version explanation" section describing
host_rocm_version_newer_than, theversion_note:/warning:lines in both the wheel and tarball paths,newest_repo_version/repo_version_without_wheels, and thedetect_legacy_rocm_summaryprobe. - Coverage (
b64d39f): split the filesystem probe out ofhost_rocm_version_newer_thaninto a purehost_version_newer_thancore and routed the note/warning strings through small pure builders (wheel_host_version_note,tarball_host_version_note,no_wheel_warning_message). Addedhost_version_newer_than_reports_only_a_strictly_newer_host(the newer-than decision across newer/equal/older/none) andhost_version_notes_and_warning_render_the_expected_text(asserts the exactversion_note/warningoutput text for both paths). No behavior change.
(The branch was force-pushed after re-signing the original commit, which is content-identical to before; the test commit is the only new content.)
There was a problem hiding this comment.
Re-verified at 62105bb. Nothing changed here since your re-read at b64d39f; confirming the disclosure and the coverage still hold at the current head, and that the correctness fix from the later round landed inside this same feature:
- Disclosure: the PR description's "Also in this PR: host-ROCm-vs-TheRock version explanation" section still names
host_rocm_version_newer_than, theversion_note:/warning:lines on both the wheel and tarball paths,newest_repo_version/repo_version_without_wheels, and thedetect_legacy_rocm_summaryprobe. - Coverage:
host_version_newer_than_reports_only_a_strictly_newer_host(therock.rs:6110) andhost_version_notes_and_warning_render_the_expected_text, alongsiderepo_version_without_wheels_warns_only_when_newest_is_newer. - The gap the tests then exposed: the newer-than decision was still routed through
compare_version_strings, which degraded to a lexicographic compare for build-suffixed and two-component host versions. That is fixed (lenientparse_host_version,Nonewhen either side is unparseable) and the test now covers the realistic host shapes — see the reply on thetherock.rs:1215thread.
So the "only new logic without direct test coverage" is no longer that, and the coverage found a real bug rather than just recording current behavior.
Green on Linux at 62105bb: cargo test --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings; cargo xtask e2e on a GPU host passes runtime-08.
0a2a239 to
b64d39f
Compare
|
Re-reviewed at head Prior scope finding: resolved. The bundled host-ROCm-vs-TheRock version-note feature (
That resolves the concern as raised (thin/missing tests, no disclosure); the extra unconditional Rest of the diff ( No new findings at reviewable confidence. Looks safe to approve after final CI validation — note the self-hosted GPU E2E lane is currently red at this head ( |
|
Thanks for the thorough re-read at |
|
@rominf the bundled version-explanation feature is now disclosed and covered, in
Ready for another look when you get a chance — thanks. |
rominf
left a comment
There was a problem hiding this comment.
I took another pass over this at b64d39f, this time building and running the version comparator and tracing every install sdk argv call site rather than reading them. That turned up real problems, so I'm reversing my earlier comment on this same head ("no new findings at reviewable confidence; looks safe to approve after final CI validation") — that was wrong, and the details are in the inline comments below.
The core issue is that the new RefuseNonInteractive bail is a hard failure for callers that never got the --yes flag. interactive_terminal() is stdin().is_terminal() && stdout().is_terminal(), and its own doc comment names this exact case: "the dashboard spawning rocm as a child" makes it false. Every non-TTY caller of install sdk in this repo — the chat approval arm, the install_sdk MCP tool, both TUI install paths, the e2e step definition, and the prewarm xtask — still emits a bare install sdk. Once a runtime exists, all of them now bail instead of installing. The chat arm is the clearest tell: its four sibling arms all call ensure_flag(&mut args, "--yes") and the SDK arm doesn't.
Beyond that: the new host_version_newer_than feeds host version strings into a comparator written for PyPI index versions, and that comparator falls back to a plain string compare when either side doesn't parse. Host versions frequently don't parse (build suffixes, two-component forms), and the fallback reports "host is newer" whenever the host string sorts lexicographically above the resolved one. 7.2.4-98 vs 7.13.0 is a false positive — and 7.2.4 is exactly what the GPU runner reports today (legacy_rocm_version: 7.2.4 in the lane on this head).
Two smaller ones: the cancel path returns mutated: false but the audit log still records "sdk install completed", and the new scenario 7 asserts only things its own Given already guarantees.
Merge state: this PR is CONFLICTING/DIRTY against main. git merge-tree --write-tree origin/main <head> conflicts in tests/e2e-cucumber/tests/e2e/runtime_steps.rs — the base is 18 commits behind, and main rewrote user_installs_sdk to route through run_rocm_with_scenario_env with a cli_failure_report while this branch rewrote the same function to add --yes. This is a recurrence of the conflict flagged in the 2026-08-26 review on an earlier head. Whatever the checks say at b64d39f, they do not validate the merge result — worth re-running after the rebase.
CI at this head: the GPU lane is red — 85 scenarios, 9 failed, 4 unexpected (bench-load-real-serve, chat-end-to-end-local-model, serve-vllm-inference, serve-vllm-default-on-instinct). All four are serve/inference, none touch install or --yes, so they look unrelated to this change — but the lane still needs to end green. Note also that scenario 6 passed there while scenario 7 never ran: it's @nightly, so it only executes under E2E_INCLUDE_NIGHTLY=1 and gets no per-PR signal at all.
A couple of things I suspected and checked that did not hold up, for the record: the scenario numbering in runtime_setup.feature does not collide with main (head has 1,3,4,5,2,6,7; main has 1,3,4,9,5,8,2), and the 2-minute subprocess timeout on approved installs is pre-existing on main, not introduced here.
| /// Approve overwriting an existing ROCm SDK (and required system-package | ||
| /// installs such as OpenMPI for vLLM) without prompting; required to | ||
| /// overwrite an existing SDK outside an interactive terminal. A fresh | ||
| /// install (no existing SDK) never prompts. |
There was a problem hiding this comment.
The --yes flag never reaches the non-interactive callers, so they now hard-fail.
(Anchored here on the new flag doc — the actual sites are listed below, all outside this diff.)
Every one of these spawns rocm with .stdin(Stdio::null()), so interactive_terminal() is false, so with an existing runtime the new SdkInstallApproval::RefuseNonInteractive arm bails with "an existing ROCm SDK would be overwritten; re-run with --yes" — a flag the user has no way to supply through these surfaces:
apps/rocm/src/main.rs:10212— the chatinstall sdkarm returnsChatRocmCommandAction::Approval { args, .. }without touchingargs. Its four siblings (:10220driver,:10256,:10283,:10297) all callensure_flag(&mut args, "--yes")first. Path:dash_seam.rs:64 execute_approved->run_internal_mcp_call(.., true)->main.rs:11261->run_rocm_capture_for_paths->main.rs:11440-11463(Stdio::null()stdin).apps/rocm/src/main.rs:12021-12043—rocm_chat_tool_requested_argsfor theinstall_sdkMCP tool, same story viamain.rs:11356.crates/rocm-dash-tui/src/ui/install_manager.rs:136-158build_args()— never adds--yes; spawned bycrates/rocm-dash-tui/src/jobs.rs:73-79with null stdin.crates/rocm-dash-tui/src/ui/onboarding.rs:185-200build_install_args— same.
Adding ensure_flag(&mut args, "--yes") to the chat/MCP arms matches the existing convention exactly. The TUI paths need the same treatment (or an explicit confirm step in the TUI that then passes --yes). Either way this should be covered before merge, because the failure mode is "reinstall silently stops working from the dashboard" rather than anything loud.
There was a problem hiding this comment.
Verified at 62105bb — fixed on the prior head (82281a6), confirmed against current code. All four sites you listed now carry the flag:
- chat
install sdkarm —main.rs:11537ensure_flag(&mut args, "--yes"), matching theinstall driverarm immediately below it. - MCP
install_sdktool —rocm_chat_tool_requested_args(main.rs:13352) pushes--yesinto the argv it builds. It does not route through the classifier, so it needed its own fix. install_manager.rs:153build_args()—--yeson the real-install branch only; the dry-run branch stays flag-free, since a preview never mutates and so never reaches the gate.onboarding.rs:193build_install_args.
Consent is preserved rather than bypassed: the chat/MCP paths still return ChatRocmCommandAction::Approval, and the rendered command shown in that approval now includes --yes (asserted in rocm_chat_tool_requested_command), so the user approves the overwriting command explicitly.
Regression test: install_sdk_chat_and_mcp_args_carry_yes_for_non_interactive_spawn (main.rs:22367) covers both the classifier arm and the MCP args builder, which previously had none. The TUI paths are asserted in their own crates (install_manager.rs:489, onboarding.rs:679/927/957/994), including the negative case that the dry-run preview does not carry the flag.
One site I checked and left alone: internal_mcp_install_sdk_args (main.rs:13034) has no --yes, but both its callers pass dry_run: true, so it can never reach the approval gate.
Green on Linux at 62105bb: cargo test --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings.
| /// version (if any) and the version about to be installed, return the host | ||
| /// version only when it is strictly newer. Split out from the filesystem probe so | ||
| /// the newer-than decision is unit-testable without a real legacy ROCm on disk. | ||
| fn host_version_newer_than(host_version: Option<String>, resolved_version: &str) -> Option<String> { |
There was a problem hiding this comment.
host_version_newer_than reports "host is newer" for a large class of realistic host versions.
compare_version_strings (:3686-3693) falls back to a raw string compare when either side fails parse_version:
match (parse_version(left), parse_version(right)) {
(Some(l), Some(r)) => l.cmp(&r).then_with(|| left.cmp(right)),
_ => left.cmp(right), // lexicographic
}parse_version (:3695-3728) requires MAJOR.MINOR.PATCH[rcN|aN], so build-suffixed and two-component host versions return None. That comparator was written for PyPI index versions, which always have that shape; host versions don't. I extracted these four functions verbatim into a scratch binary and ran them:
host=7.14.0 resolved=7.13.0 parses=true -> HOST NEWER (correct)
host=7.2.4-98 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.2.4-98 resolved=7.14.0 parses=false -> HOST NEWER (WRONG)
host=7.4 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.9 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.2 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.13.0-56 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.9.0-1 resolved=7.13.0 parses=false -> HOST NEWER (WRONG)
host=7.9.0-1 resolved=7.13.0a20260416 parses=false -> HOST NEWER (WRONG)
host=6.4.1-123 resolved=6.13.0 parses=false -> HOST NEWER (WRONG)
host=6.4.1-123 resolved=7.13.0 parses=false -> ok
host=5.7.1-4 resolved=7.13.0 parses=false -> ok
host=7.0.0-77 resolved=7.13.0 parses=false -> ok
The rule is just "does the host string sort above the resolved string" — "7.2.4-98" > "7.13.0" because '2' > '1'. This isn't hypothetical: the GPU runner reports legacy_rocm_version: 7.2.4 today, so a build-suffixed variant of it is the expected shape, and any 7.2/7.4/7.9 host lands in the wrong bucket against a 7.13 resolution.
The new unit test at :5609-5622 only uses clean three-component versions, which is why it passes.
Suggested fix: don't route host versions through the index comparator. Parse the host version leniently (strip a -<build> suffix, tolerate two components) and return None — i.e. "can't tell, don't claim newer" — when it still doesn't parse, rather than silently degrading to a string compare. Whatever shape the fix takes, the table above makes a good test case list.
There was a problem hiding this comment.
Verified at 62105bb — fixed on the prior head (82281a6); re-checked the implementation against your table rather than taking the earlier reply on trust.
The lexicographic fallback is gone from this path. host_version_newer_than (therock.rs:1215) no longer calls compare_version_strings; it parses both sides with a new lenient parse_host_version (therock.rs:4060) and returns None if either fails:
let host_parsed = parse_host_version(&host_version)?;
let resolved_parsed = parse_host_version(resolved_version)?;
(host_parsed > resolved_parsed).then_some(host_version)parse_host_version splits off build/local metadata on +/- (7.2.4-98 -> 7.2.4), tolerates a two-component report by defaulting patch to 0 (7.4 -> 7.4.0), and still accepts an rc/a stage suffix so a TheRock alpha such as 7.13.0a20260416 compares correctly on the resolved side. Anything it cannot read is "can't tell", not "newer" — compare_version_strings keeps its lexicographic tiebreak for the PyPI index sorting it was written for, which is untouched.
Your table is the test list: host_version_newer_than_reports_only_a_strictly_newer_host (therock.rs:6110) asserts not-newer for 7.2.4-98, 7.4, 7.9, 7.13.0-56 and unknown against 7.13.0, newer for 7.14.0 and for 7.20.1-33 (returning the original unstripped string, so the user-facing note still shows what the host actually reported), and the equal/older/None cases.
Green on Linux at 62105bb: cargo test --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings.
| } else { | ||
| None | ||
| }; | ||
| print!("{output}"); |
There was a problem hiding this comment.
A cancelled install is recorded in the audit log as completed.
(Real site is :2431-2444 in this file, just below this hunk.)
SdkInstallResult carries mutated, and the new PromptOverwrite decline path in therock.rs:1015-1020 returns Ok(SdkInstallResult::plan(output)) with mutated: false after printing status: cancelled by user; the existing ROCm SDK was left unchanged. But :2431-2444 records record_cli_audit_event(&paths, "runtime", "install_sdk", "info", "sdk install completed channel=... dry_run=...") unconditionally.
So a user who answers "no" at the prompt gets an audit trail saying the install completed. Gating that record on mutated (or emitting a distinct cancelled event) would keep the log truthful — it's a small change but audit records are exactly the thing that has to be right.
There was a problem hiding this comment.
Verified at 62105bb — fixed on the prior head (82281a6). The audit detail is now derived from mutated rather than being hardcoded (main.rs:2470):
let status = if dry_run || mutated { "completed" } else { "cancelled" };
format!("sdk install {status} channel={channel} ...")So the decline path in therock.rs:1035-1041, which returns SdkInstallResult::plan(output) with mutated: false, is recorded as sdk install cancelled .... A dry-run keeps completed, since it legitimately completes a preview and never claimed to mutate.
The same mutated value also gates finalize_successful_sdk_install, so a declined install records no finalization and — because finish_sdk_install only runs the auto-install callback when finalized is Some — does not go on to auto-install an engine either.
I kept it as a status word inside the existing install_sdk action rather than a separate event type, so the existing action taxonomy (install_sdk / install_sdk_dry_run, plus the error severity on the failure arm) stays intact and a reader greps one action for the whole outcome space.
Green on Linux at 62105bb: cargo test --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings.
| # active runtime. Outside an interactive terminal (as every e2e invocation | ||
| # is here), `install sdk` without `--yes` must refuse rather than overwrite. | ||
| # Cheap even though GPU-gated: the precondition needs a GPU to have a runtime | ||
| # active, but the refusal itself bails before any download. |
There was a problem hiding this comment.
This comment isn't accurate — the refusal does not bail before downloading.
The claim is that the scenario is cheap because "the refusal itself bails before any download". Tracing the wheel path in therock.rs, the approval gate is at :1007, but it runs after:
:831resolve_python_launcher(paths)?->ensure_managed_python->ensure_uv_binary(crates/rocm-core/src/uv.rs:396-427,download_file_to_path) anduv python install:855resolve_pip_runtime(...)->load_simple_index_versions->download_text_cached
Both can hit the network before the gate is ever reached. The tarball path at :1389-1424 has the same ordering. On a warm runner the caches usually absorb this, but the scenario isn't structurally download-free, and the comment will mislead whoever next reasons about the gating.
Either reword to match reality, or — probably better — move the approval gate ahead of launcher/index resolution so the refusal genuinely is free. That would also make the CLI behavior better: today a user who is going to be refused still pays for a uv download and an index fetch first.
There was a problem hiding this comment.
Reworded again at 62105bb — the previous rewording still overstated the case with "(both cheap)", which glosses exactly what you flagged.
On why the gate stays where it is: it cannot move ahead of resolution. existing_runtime_relation is called with resolution.family and resolution.latest_version, and the whole point of the gate is to name which runtime would be overwritten (upgrade/downgrade/reinstall from installed X (key)). Moving it earlier would mean either dropping that identity or refusing installs that are not overwrites at all — a different resolved family/version is a side-by-side install, not a clobber. So the ordering is load-bearing, and the comment is the thing that had to become honest.
The comment now reads:
# The refusal is not free: the gate keys on the resolved family and version, so
# it runs after the Python launcher is resolved and the channel index is read.
# Both are already warm here — the `Given` installed a runtime, so the launcher
# resolves to the saved managed Python rather than bootstrapping uv, and the
# index read is cached — but on a cold host the launcher step can still fetch.
# What the refusal does bail before is the SDK and torch download and any
# change on disk.
That is checkable against the code: resolve_python_launcher_in (therock.rs:3862) returns the saved managed Python from load_managed_python_manifest before it can reach ensure_managed_python, and this scenario's Given guarantees one exists — but the cold path you traced is real, so the comment now names it instead of calling it cheap. ensure_uv_binary itself is at therock.rs:1052, after the gate.
runtime-08 passed on a GPU host in cargo xtask e2e at this head.
| Given a managed runtime is active | ||
| When the user reinstalls the SDK with --yes | ||
| Then a runtime is registered | ||
| And the runtime is set as active |
There was a problem hiding this comment.
Scenario 7 passes without testing anything the Given didn't already establish.
The only assertions are Then a runtime is registered and And the runtime is set as active. The step defs (runtime_steps.rs:431-438 and :440-452) just check !stdout.contains("installed: none") and a non-empty active_runtime_key — both of which are already true after Given a managed runtime is active, before the --yes reinstall runs at all. Delete the When step and the scenario still passes, which means it can't fail if --yes regresses.
To actually cover the overwrite it needs to assert something that distinguishes before from after — e.g. that the Overwriting existing ROCm SDK (...) with ROCm ... line appears in stdout, or that the runtime key/version changed.
Worth flagging that this is @nightly, so it doesn't run per-PR at all (E2E_INCLUDE_NIGHTLY=1 gate, see tests/e2e-cucumber/README.md:120). Combined with the vacuous assertion, the --yes overwrite path currently has no CI signal in either direction. This is part of a broader pattern across several open PRs, tracked in EAI-8498.
There was a problem hiding this comment.
Verified at 62105bb — fixed on the prior head (82281a6), and this time with a real CI result behind it.
The scenario (renumbered runtime-09) now asserts a step that distinguishes before from after:
Then the install reports overwriting the existing runtimewhose step (runtime_steps.rs:470) asserts Overwriting existing ROCm SDK in stdout — the only externally visible signal of the ProceedApproved branch (therock.rs:1023). Deleting the When now fails the scenario, and so does a regression of --yes to a refusal or to the fresh-install path (which prints No existing ROCm SDK found instead).
On the CI-signal half of your point, which I think is the more important one: the companion runtime-08 (@requires-gpu, not @nightly) is the one that carries the signal per-run, and it is not vacuous — it asserts a non-zero exit and that the error names --yes. I ran the suite on a GPU host at this head and it passed there:
Scenario: runtime-08 - Reinstalling the SDK over an existing runtime without --yes is refused
✔ Given a managed runtime is active
✔ When the user reinstalls the SDK without confirming
✔ Then the reinstall is refused
✔ And the error explains that --yes is required
runtime-09 stays @nightly because it performs a real second multi-GiB SDK install; the refusal direction is what regresses silently, and that one now runs on the GPU lane every time. Happy to follow whatever comes out of EAI-8498 on the broader nightly-signal question.
| } | ||
| assert_engine_ready(world); | ||
| } | ||
|
|
There was a problem hiding this comment.
Two non-interactive CI callers still invoke bare install sdk.
runtime_steps.rs:59(setup_runtime_with_engine, just above this hunk) still callscrate::run_rocm_ok(world, &["install", "sdk"]).xtask/src/e2e_prewarm.rs:218still calls.args(["install", "sdk", "--channel", channel]).
Both run non-interactively in CI, so both hit RefuseNonInteractive as soon as a runtime already exists. The prewarm one is reachable with a non-empty tree via the "nothing installed for this channel" branch of decide() (e2e_prewarm.rs:105-110), so a shared tree that already has one channel installed will now fail when prewarming a second.
Adding --yes to both is the same one-line change already made to the sibling steps in this PR.
There was a problem hiding this comment.
Re-verified both sites at 62105bb. The conclusion changed on re-check, so the detail matters:
runtime_steps.rs — flag added (62105bb). You are right that the asymmetry was a real gap in this PR's own diff: a managed runtime is active gained --yes here while its sibling a managed runtime with an inference engine already installed kept the bare form. 62105bb adds --yes to the sibling. It is a no-op today — both installs sit behind if stdout.contains("installed: none"), so they can only ever be fresh installs, and a fresh install never reaches the gate — but leaving one of two identical call sites unflagged is a trap for whoever loosens that guard.
xtask/src/e2e_prewarm.rs — deliberately not changed. The failure you traced does not exist, because the gate is narrower than install sdk seeing any runtime. existing_runtime_relation (therock.rs:1177) filters manifests by manifest.family == family && manifest.channel == channel.as_str(), so it only returns Some for a runtime matching both the resolved family and the channel being installed. The decide() branch you cite is entered precisely when no runtime line carries channel=<target>, so there is no manifest for that channel and the gate resolves to ProceedFresh. A shared tree holding another channel prewarms a second one without refusal.
Adding --yes there would also be the wrong direction. Decision::Install is only chosen when the pre-warm believes nothing is installed for this channel; if that belief is ever wrong, a refusal is the correct loud signal, whereas --yes would silently overwrite a multi-GiB shared runtime other scenarios are using.
Verified on Linux at 62105bb: cargo test --workspace --all-targets and cargo clippy --workspace --all-targets -- -D warnings clean, and cargo xtask e2e on a GPU host ran runtime-08 green.
| args.channel, | ||
| "--format", | ||
| "wheel", | ||
| "--yes", |
There was a problem hiding this comment.
Docs still show the pre---yes invocation.
This script was updated, but the user-facing docs weren't:
README.md:196,:221,:251docs/testing.md:156
All four document rocm install sdk with no --yes. Anyone following them non-interactively — or re-running the documented command over an existing runtime in a script — now gets the refusal. Since this PR changes the contract for that exact command, the docs should move with it.
There was a problem hiding this comment.
Verified at 62105bb — fixed on the prior head (82281a6). All four locations you listed now document the post---yes contract:
README.md:199-204— the "Configure ROCm and serve a model" block keeps the barerocm install sdk(correct: that is a first-time, fresh install, which never prompts) and adds the sentence explaining that re-running it over a managed runtime asks before overwriting, and that--yesapproves that non-interactively.README.md:255— the synopsis lists[--yes].README.md:262-266— theinstall sdkprose states the fresh-vs-overwrite split and that the prompt refuses in scripts or CI.docs/testing.md:156— the live acceptance-test invocation now shows--yes, with a note on why (the test root is reused across runs) and a pointer toscripts/therock_sdk_install_test.pyso the two cannot drift.
I deliberately did not add --yes to every documented rocm install sdk occurrence — the quick-reference table row and the rocm examine prose describe the command generically, and blanket-flagging them would teach readers to paste an overwrite approval into first-time installs where it does nothing.
Green on Linux at 62105bb: cargo test --workspace --all-targets, cargo clippy --workspace --all-targets -- -D warnings.
…d update tests Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…I-7326) The legacy-ROCm-vs-TheRock version explanation (host_rocm_version_newer_than and the version_note/warning lines it drives in both the wheel and tarball install paths) shipped without direct coverage. Split the filesystem probe out of host_rocm_version_newer_than into a pure host_version_newer_than core, and route the version_note/warning strings through small pure builders. Add unit tests for the newer-than decision and the exact note/warning wording. No behavior change. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
b64d39f to
7e2513f
Compare
|
Thanks for the review — rebased onto 1.
2. 3. Cancelled install recorded as 4. 5. Scenario 7 vacuous. Added a 6. Merge conflict vs Gates green locally: |
… honest audit + scenario (EAI-7326) Non-interactive callers (dashboard install manager, onboarding wizard, chat/MCP install_sdk) now pass --yes so a null-stdin spawn is not refused at the overwrite prompt. host_version_newer_than parses both versions leniently (build suffix, two-component) and returns None when either cannot be parsed, instead of a lexicographic compare that wrongly ranked e.g. 7.2.4-98 above 7.13.0. A declined real install is recorded as 'cancelled' rather than 'completed' in the audit trail. Reworded the runtime_setup overwrite-refusal comment to match where the refusal actually bails, and gave scenario 7 a distinguishing 'reports overwriting' assertion so it can no longer pass as a no-op. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
7e2513f to
46d14a8
Compare
…umbing (EAI-7956) The --yes contract change for `rocm install sdk` was not reflected in the user-facing docs, and the chat/MCP arms that inject --yes had no regression test. - README.md: add [--yes] to the install sdk synopsis and explain that re-running over an existing managed SDK prompts (or refuses when spawned non-interactively) unless --yes is passed; note the same in the quickstart. - docs/testing.md: the live SDK acceptance test command now shows --yes, matching scripts/therock_sdk_install_test.py, so a reused test root is not refused at the overwrite prompt. - apps/rocm: add a regression test asserting both chat_rocm_command_action_from_args and rocm_chat_tool_requested_args inject --yes for install sdk, so the dashboard/assistant reinstall path cannot silently regress to a refusal. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Thanks for the review. Addressed in
|
…956) `a managed runtime with an inference engine already installed` was the one shared-tree install step this PR left on a bare `install sdk`, while its sibling `a managed runtime is active` gained `--yes`. The `installed: none` guard means it is a fresh install today, so neither shape can reach the overwrite gate, but the asymmetry is a trap: the harness spawns `rocm` with null stdin, so if that guard ever loosens the step fails at a prompt nothing can answer instead of proceeding. Also correct the Scenario runtime-08 comment. It claimed the launcher and index resolution ahead of the approval gate are "both cheap"; on a cold host the launcher step can bootstrap a managed Python and download uv. The gate keys on the resolved family and version, so it cannot move ahead of that resolution. Say what is actually true: warm here because the Given installed a runtime, and what the refusal genuinely bails before is the SDK/torch download and any on-disk change. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 62105bb
Summary
Adds a --yes flag and an overwrite-confirmation gate to rocm install sdk, propagates it to the non-interactive spawn surfaces (chat, MCP, dashboard, onboarding, e2e, acceptance script), and separately adds "host ROCm is newer" / "repo newest has no wheels" explanations. Needs work — one non-interactive surface was missed, and the gate's user-facing wording contradicts what the code actually does. Verified: read the full diff plus surrounding code in therock.rs, main.rs, rocmd/lib.rs, the TUI crates and the e2e harness, and answered the revert question for every added/changed test — all of them are discriminating (each calls the real production symbol and asserts a literal the change produces; the 7.2.4-98 vs 7.13.0 case at apps/rocm/src/therock.rs:725 genuinely fails under the old lexicographic compare, and the e2e Thens at runtime_steps.rs:639 and :477 are not satisfiable by pre-fix output, which contains no --yes at all). Relied on the CI result for the full matrix and ran no suites myself. No prompt-injection content found in the diff. Blocking: 2 · Non-blocking: 6.
🚫 Blocking (must fix before merge)
apps/rocmd/src/lib.rs:2541-2548—build_install_sdk_argsbuildsinstall sdkargv without--yes, andrun_rocm_capture_for_paths(apps/rocmd/src/lib.rs:2376) spawns the child with.stdin(Stdio::null()). The"install_sdk"tool atapps/rocmd/src/lib.rs:2217therefore hitsSdkInstallApproval::RefuseNonInteractiveand bails with "re-run with--yes" — a flag no caller of that MCP tool can supply. This is a second, independent implementation from theapps/rocmchat/MCP builders the PR did patch, so commit 82281a6's claim to have covered "chat/MCP plumbing" is incomplete and the new regression test atapps/rocm/src/main.rs:22367does not reach it. Fix: push"--yes".to_owned()in theargvvec atapps/rocmd/src/lib.rs:2541for the non-dry-run path, and extend the existingbuild_install_sdk_argstests nearapps/rocmd/src/lib.rs:5859to assert it.apps/rocm/src/therock.rs:190,:210-213,:375,:388(and mirrored at:460,:481-484) — the gate tells the user an existing SDK "would be overwritten", but for an upgrade or downgrade nothing is overwritten.runtime_keyembeds the resolved version (apps/rocm/src/therock.rs:4184-4196), soinstall_root(:4224-4232) and the manifest path (:4238-4240) are version-distinct: the old install and its manifest survive intact and only the active-default pointer moves. Only a same-version reinstall is a true overwrite. The function's own docstring at:231-234says "displace as the active default" — the messages contradict it, and theRefuseNonInteractivebail hard-fails a non-destructive side-by-side upgrade in any script or CI lane. Fix: either reword all four sites to the accurate effect ("ROCm X is already installed for this family/channel; installing Y will become the active default, replacing it"), matching the prompt body at:383which is already correct — or narrowexisting_runtime_relationto the same-runtime_keycase if a literal overwrite is what the gate is meant to guard.
Non-blocking
apps/rocm/src/therock.rs:240-249vsapps/rocm/src/main.rs:8830-8841— the gate filters manifests by family+channel, butfinalize_successful_sdk_installactivates the globally newest manifest unfiltered, so installing a different family or channel silently takes over the active default with no prompt and no--yes; the gate is narrower than the effect it guards.apps/rocm/src/therock.rs:113and:164—host_rocm_version_newer_thanrunsdetect_legacy_rocm_summarytwice per real install, uncached, each doingread_dirover/optand/usr/localplus per-candidate marker probes; both calls happen even on the refused path. Compute once and reuse.apps/rocm/src/therock.rs:839—resolve_python_launchercan reachensure_managed_python(:3910), downloading and installing a Python interpreter to disk, before the confirmation gate at:180; a declined or refused install can still leave that behind.apps/rocm/src/therock.rs:742-763—host_version_notes_and_warning_render_the_expected_textasserts three pure format functions return their own format strings verbatim; it fails on deletion but has near-zero defect-detection power and locks in copy wording.tests/e2e-cucumber/features/runtime_setup.feature:157— runtime-09 is@nightly, gated behindE2E_INCLUDE_NIGHTLY(tests/e2e-cucumber/tests/e2e.rs:1085) which.github/workflows/e2e-selfhosted.ymlonly sets onworkflow_dispatch, so it did not run in this PR's checks;AGENTS.md:98-99requires naming that lane in the PR text.- Scope: the diff bundles the
--yesgate with an unrelated host-version-note / no-wheels-warning feature (newest_repo_version,parse_host_version, the three message builders); worth splitting, andxtask/src/e2e_prewarm.rs:417still spawns a bareinstall sdkthat is safe only bydecide()'s current invariant.
…t gate wording (EAI-7956) The `install_sdk` MCP tool in `apps/rocmd` builds its own `install sdk` argv, independently of the `apps/rocm` chat/MCP builder this PR already patched, and `run_rocm_capture_for_paths` spawns the child with null stdin. With an existing managed SDK for the family/channel the approval gate therefore resolved to `RefuseNonInteractive` and bailed asking for `--yes`, a flag no MCP caller of that tool could supply. Push `--yes` on the non-dry-run path only; the dry-run path returns before the gate. Consent is unchanged: `install_sdk` is in `mcp_tool_requires_direct_approval`, so a direct `rocmd mcp-call` still needs `--allow-mutation` after an explicit user approval. The gate's user-facing text also claimed an overwrite that does not happen. `runtime_key` embeds the resolved version, so an upgrade or downgrade gets its own install root and manifest and the previous install survives; only the active-default pointer moves. Reword the approved-install line, both refusal bails and the interactive prompt to state that effect, matching the wording the `existing_runtime_relation` docstring and the prompt's `effect:` line already used, and update the README, testing doc and the runtime-09 scenario to match. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…nsent The chat arm stripped a model-supplied `--yes` by exact string match, so it only stayed airtight because clap rejects `--yes=true` on a bare `bool`. That borrowed a property of clap's error taxonomy which this code does not own: a later `num_args` on the flag would silently restore the system-package/sudo consent 76c6aa3 removed, on a spawn with null stdin and no terminal to answer a password prompt. Match the `--yes=` prefix too, so the guarantee is local. Retarget the test that named the strip but never called it. It pinned the clap rejection only, which is the backstop rather than our code; it now drives `chat_rocm_command_action_from_args` with a model-supplied `--yes=true` argv and asserts the flag does not survive, keeping the clap assertion as an explicitly secondary layer. Without the prefix term the new test fails with the flag sitting in the spawn argv next to the injected consent. Tell the operator why `rocm --yes <request>` prints two differing `tool_call:` lines. The difference stays deliberate for the reason already documented -- forcing agreement would reach `render_structured_request_plan`, shared with the no-`--yes` review path, and hand a pre-approved command to a human asked to review it -- but the explanation lived only in a doc comment, which reaches the next reader of the file and not the operator looking at the two lines. `apply_freeform_execution_consent` now reports whether it added the flag, and the execution section says so in words when it did. Pin the `default_runtime_id_match_count != 1` invariant with a `debug_assert` rather than prose: at exactly 1 the message would tell the operator no manifest matched the recorded default while one did. Apply `make_test_runtime_manifest_unparsable` to the sibling test that still inlined the identical fixture-corruption steps. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
All five items verified against the code first, all five held. Addressed in Items 2 + 1 (the pair). Confirmed the strip was exactly The criticism of the test was fair: it named the strip and never called it. It now drives Falsified by reverting the That is the flag sitting in the null-stdin spawn argv next to the injected consent. Restored, green.
Both other changed tests falsified too — stubbing the note render and dropping the helper call each redden their test ( No behaviour change to the fail-closed-into-the-consent-gate policy. Validation on Linux: |
|
🔴 Automated review · pr-review-watcher · 4a53615 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryAdds a 🚫 Blocking (must fix before merge)None. Non-blocking
Check state: this review worked from 19 success / 0 failure / 0 pending on this commit, and immediately before posting the same commit reports the same 19 success / 0 failure / 0 pending. |
…guards The `note:` line 4a53615 added under the `execution` header is command output, and AGENTS.md 3 requires user-observable behaviour to be covered by a Gherkin scenario, not only a unit test. Scenario runtime-15 drives the real binary through `rocm --yes <request>` and pins all three halves of the claim: the `request plan` command carries no replacement consent, the executed one does, and the note says where it came from. A note promising a difference is a lie if the two lines agree, so the first two Thens are what make the third one mean anything. The scenario must not actually install: on a GPU lane that request resolves to a real multi-GiB SDK pull, and the assertions are about output printed before dispatch. The Given points `ROCM_CLI_PYTHON` at a path that does not exist, so `resolve_python_launcher` -- the first step of `install sdk` -- fails offline, instantly, and after the header is on stdout. Without the note the scenario fails on its last step. Retarget the over-broad-match guard in the chat strip test. It fed `--prefix /tmp/--yes-not-a-flag`, a value that starts with neither `--yes` nor `--yes=`, so it was kept by the exact-match strip that preceded this PR, by the two-term strip that replaced it, and by the widened `starts_with("--yes")` it was meant to rule out -- it pinned nothing at all. A bare `--yes-not-a-flag` token is eaten by that widening and kept by the shipped match, and the comment now says plainly that this guards the next edit rather than this one. Drive both terms of the strip from the test named for it. It fed only `--yes=` forms, leaving `arg != "--yes"` to a sibling test named for dry runs; the loop now covers the bare form too, so dropping either term reddens this test alone. Apply `make_test_runtime_manifest_unparsable` to the last test still inlining its fixture-corruption steps, finishing the de-duplication. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
All four non-blocking items addressed in Item 1 — scenario for the The install must not actually run (on the GPU lanes that request resolves to a real multi-GiB SDK pull) and the assertions are about output printed before dispatch, so the Falsified by deleting the Restored, it passes 5/5. Item 2 — the guard that could not fail. Confirmed: Item 3 — both halves covered directly. The loop now feeds Dropping Dropping Both restored after. Item 4 — leftover duplication. Also noted. Left alone: the two Validation (Linux devbox): |
|
🔴 Automated review · pr-review-watcher · 27eef66 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummarySplits 🚫 Blocking (must fix before merge)None. Non-blocking
Check state at the time this review was prepared: 16 success, 2 pending, no failures. Re-read immediately before posting: 20 success, 5 pending, 1 failure. The failure appeared after the review was under way, so it is not covered above and is worth a look before merging. |
One conflict, in `therock.rs`'s `rocm_core` import list. Both sides only added: this branch needs `detect_legacy_rocm_summary` and `interactive_terminal`, #405 added `RUNTIME_LIBRARY_PATH_ENV`. Took the union. `detect_legacy_rocm_summary` is the one worth naming: it is `pub` only on this branch — main still declares it private — so the union compiles only because the `lib.rs` auto-merge kept this branch's `pub`. Verified that, and that all three symbols have real call sites in the merged file, since an unused import would fail the `-D warnings` gate. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
One conflict, in `main.rs`'s test module: both sides appended a `#[test]` at the same point, so git spliced them and the two functions shared a trailing `);` / `}`. Union — they test unrelated things. This branch's three `--yes`/consent help tests are kept whole, and #328's `runtimes_help_uses_the_runtime_noun_throughout` is reattached with its own closing lines rather than borrowing theirs. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Re-reviewed at The consent gate no longer fails open on an unparsable active manifest. With one managed runtime active and a required field ( That also resolves the wrong-file blame I raised: with
and I confirmed separately that the injected flag actually clears the gate ( Also confirmed fixed since my last pass: the device-target check now runs before the consent gate (a host with no detectable GPU errors on the real problem instead of demanding a consent flag first — I hit exactly that path); the Nothing blocking left. Three smaller things, none of which need to hold the PR:
The One housekeeping note for whoever merges: my earlier |
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · ddc8f7a
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds --yes and --approve-replacing-active-default to rocm install sdk, together with a new consent gate that prompts (or refuses, with no terminal) before an install takes over as the active default runtime, and threads the narrow flag through every terminal-less surface. Outcome: Needs work — one documentation gap against the repo's own rule; the code itself held up under scrutiny. Verified: derived the merge base (it equals the base branch tip, so the PR-only and post-merge diffs are identical) and read all 12 changed files across five scoped passes; confirmed by reading source that the gate is not scoped to family/channel, that --dry-run returns before the gate in both the wheel and tarball paths, that --yes alone carries the sudo/system-package consent while every terminal-less caller passes only the narrow flag, and that the chat/MCP argv strip covers both --yes and --yes=… (no short alias exists to escape it); one targeted run of the new active-default tests passed (11 tests), and a single-branch mutation reverting the manifest load to the silently-dropping variant failed exactly the two tests naming that defect, so those are load-bearing; leak scan and conflict-marker scan clean; no prompt-injection content found. The full test suite, clippy and the GPU/Windows e2e suites were not run here. CI counts worked from: 19 success, 0 failure, 0 pending, 0 cancelled. Blocking: 1 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
docs/manual-testing.md:117 — the manual test plan was not updated for the behaviour this PR introduces, which the project's own AGENTS.md §5 names as required in the same change ("update README.md, its --help/doc comment, docs/testing.md, and docs/manual-testing.md in the same change — do not leave user-facing docs for a follow-up"). Section 1 of that document installs a runtime through the TUI setup, which makes it the active default; section 2 then tells the tester to run rocm install sdk --channel release --format wheel --prefix …. After this PR that command hits the new consent gate and asks for confirmation, and in a non-interactive shell it fails outright with the --approve-replacing-active-default refusal. Neither the step list nor its "Expected result" mentions either outcome, so a tester following the document hits an undocumented prompt and cannot tell whether it is the feature working or a regression. README.md and docs/testing.md were both updated for exactly this; this file is the one that was missed. Fix: add a line to section 2's expected results stating that when a managed runtime is already the active default the command asks for confirmation first, and name --approve-replacing-active-default as the way to run the step non-interactively — mirroring the wording already added at docs/testing.md:156.
Non-blocking
apps/rocm/src/therock.rs:1878— the comment introducing the gate still ends "(and needs--yeswhen there is no terminal to answer the prompt)", which contradicts the refusal message this same PR adds attherock.rs:2502and the README guidance: the narrow flag is what a terminal-less caller needs, and--yesis precisely what it must not use. The comment was rewritten twice in this branch without the stale clause being caught; the tarball path's equivalent comment attherock.rs:2635is already correct.README.md:320— "reach for it only where something can answer a sudo password prompt, which an unattended job cannot" is stated absolutely, but the repo's own unattended CI pre-warm passes--yes(xtask/src/e2e_prewarm.rs:497, justified there by passwordless sudo on the runners); softening to "unless it has passwordless sudo" would keep the two consistent.README.md:205— an absolute,main-pinned link from README.md into its own#rocm-installationsection; it is a deliberate choice (a bare anchor breaks the sliced docs build) but it will 404 on a fork and, until this merges, points at content not yet onmain.- Commit
9ee039a"carry--yesthrough freeform" is a misnomer — the code and its doc comments are clear that only--approve-replacing-active-defaultis ever carried, never literal--yes. Worth correcting if the history is tidied. - The branch carries 6 merge commits from repeatedly merging the base branch rather than rebasing, which makes the 24-commit history harder to read against
AGENTS.md§11's preference for meaningful individual commits.
Scenario runtime-13 (`runtime-install-sdk-other-family-requires-yes`) failed on
every GPU lane since `9ee039a5` moved the device-target check ahead of the
consent gate. That ordering is right — an install that can never work has to say
so rather than first demand a consent flag — but it means a *wheel* install
named with a family this host's GPU does not belong to now stops at "detected
GPU target `gfx942` belongs to family `gfx94X-dcgpu`", and the displacement the
scenario exists to prove is never reached. The step's assertion was correct and
the product is correct; the scenario was asking for the one thing the wheel path
cannot deliver on a real GPU host.
Ask by the tarball format instead. `install_tarball_runtime` resolves the
archive for the family it was given, consults no host target, and calls the same
`active_default_runtime_relation` gate, so the cross-family displacement is
reachable there with the family axis intact — no relaxed assertion, and none of
the three Thens weakened. The refusal still bails before the multi-GiB archive
is fetched; it costs an 8 KB catalog listing from the host the release wheel
index already lives on.
Tarball installs are refused outright on Windows, so the scenario gains
`@requires-os:linux`. What that gives up is this cross-family case on the Strix
Halo Windows lane only — Scenario runtime-11 still covers the refusal there. The
`@id:` tag is unchanged.
Verified on an MI300X host with a pre-warmed shared runtime tree: runtime-10
through runtime-15 all pass. Falsified twice — with the pre-`9ee039a5` step the
scenario reproduces the lane failure verbatim ("error does not name the narrow
consent flag"); with the pre-PR family-scoped gate restored the same command
installs and activates without asking at all, which the scenario's first two
Thens reject.
Separately, correct a docstring overclaim on `active_default_runtime_relation`:
it returns `None` when neither *config pointer* names an active default, not
when "nothing on disk" does. `data/runtimes/active.json` is on disk too and can
outlive both pointers through the crash window in `uninstall_runtime`, where the
config is saved before the marker is removed. Wording only — `rocm runtimes
list` shares the blind spot, and the behaviour is unchanged.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Pushed
It now asks by Cost: tarball installs are refused outright on Windows, so the scenario gains Verification. The devbox this was built on is itself an MI300X host with a pre-warmed shared runtime tree, so this is a real GPU lane rather than a simulation:
The five Linux GPU lanes should now go green; only they can confirm the families they actually resolve (the candidate is selected from Docstring overclaim. @rominf is right that "nothing on disk points at an active default" is stronger than the code:
|
|
🔴 Automated review · pr-review-watcher · 49ad726 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. This is a re-statement round, not a new gate. It reviews only the new commit That objection is unaddressed by this push and still stands: the PR changes the observable behaviour of Two notes on state at the time of writing: the checks at this head read 19 success, 0 failure, 0 pending (the review below was carried out against 18 success with 1 still pending), and the branch now reports a conflict with its base, so it needs a merge or rebase regardless of the above. SummaryThe new commit re-points scenario runtime-13 at 🚫 Blocking (must fix before merge)None. Non-blocking
|
One textual conflict, in README.md's ROCm-installation paragraph: a union of two unrelated subjects. This branch's side documents the `install sdk` consent gate, `--approve-replacing-active-default`, the extra approval `--yes` carries (sudo for system packages), and the version-keyed install root with its `--prefix` exception. #402's side documents `update`'s new `--dry-run`, that `--runtime`/`--activate` need `--apply` or `--dry-run`, the added `--json`/`--dry-run` conflict, and that `--apply` never prompts. Kept both. The rest of the resolution is semantic — this merge compiles and merges clean while quietly invalidating a premise. #402 added a `--yes` flag to `rocm update`. It is inert by its own doc comment ("applying never prompts") and the dispatch discards it (`yes: _`). So `update_has_no_yes_flag_to_credit` — which asserted that `rocm update --help` contains no `--yes` — now fails, and its own comment asked for the deliberate decision: made, and the behaviour does not change. Crediting `--yes` in the `SdkInstallApprovalSource::UpdateApply` approval line would claim an approval that flag does not grant, and on `rocm install sdk` `--yes` additionally approves running `sudo`, so the claim would be doubly wrong. What changed is the test's mechanism. It pinned a proxy (help text lacks `--yes`) that only held while the flag was absent; it now asserts the invariant itself, that neither `UpdateApply` arm's line credits `--yes`, so it still fails if that path is ever made to credit the flag. `preapproved_install_line` becomes `pub(crate)` for it. Falsified by rewriting the `activates: true` arm to say "Approved by --yes": the test fails with that line in the message. The same dead premise appears in four doc comments that said `rocm update` "has no `--yes` flag" (`install_sdk_for_update`, `SdkInstallApprovalSource`, `preapproved_install_line`, and the therock unit test). Reworded to the reason that survives: the update path's approval comes from the runtime the user selected, not from a flag. One sentence on this branch's README side died with it — "`rocm update --apply` has no `--yes` flag and needs none" — and the usage block just above it already listed `[--yes]` for `update` after the auto-merge, so leaving it would have contradicted itself on one screen. The reasoning is kept, the false premise dropped. Checked the rest of the merged surface for the same failure mode: no duplicated cucumber step definitions, no fixed-count assertions whose union is off by one, and this branch's `install sdk --yes` in `e2e_prewarm` is independent of #402's `runtimes uninstall --yes` in the same file. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
…956) AGENTS.md requires README.md, the --help text, docs/testing.md and docs/manual-testing.md to move together when observable behaviour changes. The consent gate reached only the first two. docs/manual-testing.md is the sharper gap. Section 1 leaves a managed runtime as the active default, then section 2 tells a tester to run `rocm install sdk --channel release --format wheel --prefix …`. That command now stops at the prompt, or refuses outright in a non-interactive shell, and neither the steps nor the expected result said so — a tester could not tell the feature from a regression. Section 2 now states the prompt is expected, gives the `--approve-replacing-active-default` re-run as the non-interactive route, notes `--yes` differs by also approving a sudo system-package install, and lists the prompt, the refusal, the flag-credited line, and the unaffected `--dry-run` preview as expected results. docs/testing.md named `--yes` for the live SDK acceptance test without ever naming the narrower flag, which is the one the refusal message recommends and the only one ROCm CLI's terminal-less surfaces pass. It now says why this test wants the second consent, points scripts and CI at `--approve-replacing-active-default`, gives both routes as hand checks, and records that `--dry-run` returns before the gate is consulted. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
The documentation count in this objection is resolved at bef9ea4: the manual test plan now states the prompt is expected, gives the narrow flag as the non-interactive route, distinguishes what the broad flag additionally consents to, and lists the refusal and the unaffected dry-run preview as expected results. Every one of those statements was checked against the source and holds. Retiring this so only the current objection stands; a separate change request follows on two comments that this change falsified.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · bef9ea4
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds --yes and --approve-replacing-active-default to rocm install sdk with a consent gate that prompts (or refuses, with no terminal) before an install takes over as the active default runtime, and threads the narrow flag through every terminal-less surface. Outcome: Needs work — the documentation gap we raised is properly fixed, but the branch still carries two code comments asserting premises this same PR falsified, one of them inside a merge whose message claims that sweep was completed. Verified: derived the merge base (it equals the base-branch tip, so the PR-only and post-merge diffs are identical) and reviewed all 13 changed files plus the three commits added since our last pass, including the base merge inspected with --cc because its conflict resolution carries real authored work; read the contributor rules from the base tip and confirmed the same-change documentation clause is present there verbatim; confirmed from source that every factual claim in the new docs/testing.md and docs/manual-testing.md text holds — the prompt-and-decline path mutates nothing, the refusal names the narrow flag first, the approved path credits the flag actually passed, --dry-run returns before the gate on both the wheel and tarball paths, the gate is keyed only on whether an active default exists (not on family or channel), --yes really does carry the sudo system-package consent, and < /dev/null really does drive the refusal because the terminal check ANDs stdin and stdout; confirmed the new cross-family scenario's rationale is true (the device-target refusal does precede the gate on the wheel path, and the tarball path resolves an overridden family without consulting the host target), that its three assertions are non-vacuous and would fail both on a wholesale revert and on a family-scoped-gate mutation, and that the Windows-side refusal remains covered by another scenario; confirmed the gate is fail-closed on a dangling or unparsable active-default pointer, that rocm update --yes is genuinely inert, and that the freeform argv strip covers --yes and --yes=… with no short alias to escape it; leak scan, conflict-marker scan and injection scan all clean; the checkout is byte-clean. CI conclusions worked from: 24 success, 1 failure, 1 pending; the full test suite and the GPU/Windows e2e suites were not run here. Blocking: 1 · Non-blocking: 5.
Status of our previous change request
- "the manual test plan was not updated for the behaviour this PR introduces, which the project's own
AGENTS.md§5 names as required in the same change" — RESOLVED. The clause is real: the base tip's rules say "when a command's flags, defaults, arguments, or observable behavior change, update README.md, its --help/doc comment, docs/testing.md, and docs/manual-testing.md in the same change — do not leave user-facing docs for a follow-up".docs/manual-testing.mdsection 2 now states the prompt is expected, gives the--approve-replacing-active-defaultre-run as the non-interactive route, distinguishes--yesby the sudo consent it adds, and lists the prompt, the refusal, the flag-credited line and the unaffected--dry-runpreview as expected results — the wording we asked for, and then some. Every one of those statements was checked against the source and holds. - Non-blocking,
therock.rsgate comment — STILL STANDS (see Blocking below; escalated, because the author has now written the opposite into three documents in this same PR and left the comment untouched). - Non-blocking, README's "which an unattended job cannot" — STILL STANDS, unchanged at
README.md:324while the repo's own pre-warm still passes--yesatxtask/src/e2e_prewarm.rs:496. - Non-blocking, the absolute
main-pinned self-link — STILL STANDS, unchanged atREADME.md:209. - Non-blocking, the
9ee039acommit-title misnomer — STILL STANDS; that commit is unrewritten in the range. - Non-blocking, merge-heavy history — STILL STANDS, and grew: 8 merge commits in a 27-commit range.
🚫 Blocking (must fix before merge)
apps/rocm/src/main.rs:16948 and apps/rocm/src/therock.rs:1880 — two comments still assert premises this PR itself falsified, and one of them is contradicted by the PR's own merge message.
main.rs:16948 reads "the update path is preapproved either way (there is no --yes on rocm update, and no terminal contract)". After the base merge, rocm update does take --yes (declared at main.rs:322). The merge commit explicitly enumerates this dead premise at four sites, rewords all four, and then states "Checked the rest of the merged surface for the same failure mode" — this is a fifth site, and it survived because it sits in unchanged code from the branch side rather than in a conflict hunk. A reader who trusts the comment concludes a flag does not exist that does; a reader who trusts the merge message concludes the sweep was exhaustive when it was not. Fix: reword to the reason that survives, as was done at the other four sites — the update path's approval comes from the runtime the user selected, not from a flag.
therock.rs:1880 still ends "(and needs --yes when there is no terminal to answer the prompt)". The refusal message this PR adds at therock.rs:2509 deliberately names --approve-replacing-active-default first and explains in its own doc comment that recommending --yes "would hand an unattended caller the second consent it carries — approval to install system packages with sudo". README.md, docs/testing.md and the newly added docs/manual-testing.md text all say the same. The comment tells a maintainer standing at the gate exactly what the rest of the PR tells users not to do. We named this location last round; the author then wrote three documents saying the opposite and left this line alone, which is why it is blocking now rather than a nit. The tarball path's equivalent comment at therock.rs:2645 is already correct — copy its wording, or drop the parenthetical.
Non-blocking
README.md:327— the user-facing text for--prefixsays only that "successive installs into one prefix replace each other in place"; the source comment attherock.rs:2527is franker, noting the venv there isremove_dir_all'd outright when its python no longer answers, and that no prompt fires at all for this case because the gate keys on the active default, not on the prefix folder. The deletion itself is pre-existing, but the README paragraph is new and is the natural place to say it.apps/rocm/src/main.rs:20879—update_apply_approval_never_credits_the_inert_yes_flagasserts exactly whatpreapproved_install_line_credits_the_real_consent_sourceintherock.rsalready asserts for both arms; it is a sound pin, not a vacuous one, but it adds no coverage the suite did not have.docs/manual-testing.md:139— "Use--yesonly if you also want to approve installing required system packages withsudo" sits in a section that serves both platforms and whose new code fence is labelledpowershell, but the system-package path returns early on Windows (main.rs:8239), so on that platform--yesadds nothing over the narrow flag. A Linux/WSL qualifier would prevent a Windows tester chasing an effect that cannot occur.README.md:324— "which an unattended job cannot" remains absolute whilextask/src/e2e_prewarm.rs:496passes--yesfrom an unattended job, justified in-comment by passwordless sudo on the runners; "unless it has passwordless sudo" would reconcile the two.- The range now carries 8 merge commits in 27; a rebase would make the authored history readable against the project's preference for meaningful individual commits.
Both are comments only; no behaviour changes. `apply_runtime_update` still said the update path is preapproved because "there is no `--yes` on `rocm update`". #402 added that flag in the base merge (`main.rs:322`), so the premise died there. The merge commit enumerated four sites carrying it, reworded them, and reported the sweep complete — this was a fifth, missed because it sits in unchanged branch-side code rather than in a conflict hunk, so the merge message's "Checked the rest of the merged surface" claim was wrong when written. Reworded to the reason that survives and that the other four now give: the update path's approval comes from the runtime the user selected, not from a flag. The merge commit cannot be corrected without a rewrite, so it is corrected in the PR thread instead. `install_wheel_runtime`'s gate comment told a maintainer that a terminal-less caller "needs `--yes`" — the opposite of what the refusal this branch adds says, of `refuse_non_interactive_message`'s own reason for saying it (`--yes` additionally approves installing system packages with `sudo`, whose password prompt an unattended job cannot answer), and of README.md, docs/testing.md and docs/manual-testing.md. Rewritten around the tarball path's already-correct wording, plus the refusal and the flag it names. Swept the tree for both premises: no further instances. The `--yes` requirements in `storage.rs` and `rocm-core/src/fix.rs` are a different gate on different commands, where `--yes` is the whole consent and carries no sudo approval, and are correct as they stand. Three review nits alongside them. README no longer says an unattended job can never answer a sudo prompt, since this repo's own pre-warm passes `--yes` from one and relies on passwordless sudo (`xtask/src/e2e_prewarm.rs:496`). README's new `--prefix` paragraph now admits what `confirm_overwrite_existing_sdk`'s comment already says: a venv whose python stops answering is removed outright, and the gate does not cover it because it keys on the active default, not on the folder. docs/manual-testing.md's `--yes` aside is now scoped to Linux and WSL — the section serves both platforms, but `ensure_openmpi_for_vllm` and `ensure_torch_runtime_dep` both return early on Windows, so there the second consent buys nothing. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
|
Both blocking items fixed in
The merge message's "Checked the rest of the merged surface for the same failure mode" was wrong when written, and I can't amend
Third sweep, by grep rather than assumption. Two patterns across Non-blocking, taken:
Non-blocking, skipped, with reasons:
Validated on Linux: |
Both counts are resolved at 34f7eb4, verified against the code rather than taken from the fix commit's description. The update-path comment no longer rests on the dead premise that rocm update has no --yes; the replacement reason (approval comes from the runtime the user selected) is accurate, since update declares the flag but discards it. The wheel-path comment no longer recommends --yes and now points at --approve-replacing-active-default, matching the refusal message, the docs and the tarball path's sibling comment. Withdrawing this change request.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 34f7eb4
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds a consent gate to rocm install sdk: an install that would displace the active default ROCm runtime prompts, and refuses non-interactively unless --approve-replacing-active-default (narrow consent) or --yes (which additionally approves sudo system-package installs) is passed — plus the internal non-interactive callers, docs and scenarios that follow from it. Both counts of our earlier blocking review are RESOLVED, and I confirmed the mechanism rather than taking the fix commit's word for it. Count 1 (apply_runtime_update): the comment now reads "the update path is preapproved either way (its approval comes from the runtime the user selected, not from a flag, and rocm update has no terminal contract)" — the dead "there is no --yes on rocm update" premise is gone, and the replacement is true: update does declare --yes, but the dispatch destructures it as yes: _ and discards it, so approval genuinely comes from the runtime selection. Count 2 (install_wheel_runtime): the comment now ends "With no terminal to answer the prompt it refuses instead, naming --approve-replacing-active-default as the non-interactive approval — see refuse_non_interactive_message for why that flag and not --yes" — the contradicted --yes parenthetical is gone and the text now matches the refusal message, the docs, and the tarball path's sibling comment. Outcome: No blocking findings. Verified: I read both flagged sites and their surrounding code paths, confirmed the wheel and tarball gates are call-for-call identical and that the new prose describes them accurately, traced every internal non-interactive install sdk call site to confirm each passes the narrow flag and never --yes, confirmed the two Windows early-returns the docs rely on, and ran a formatting gate check that passed cleanly with no conflict markers and no leaked internal names or injected instructions in the diff; the full suite, the end-to-end scenarios and the lint gate were not run here, so test and scenario behaviour was judged by reading the assertions against the production code rather than by executing them. Checks at this head: 18 success, 1 failure, 0 pending — I did not determine which check failed and am not guessing. Blocking: 0 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
None.
Non-blocking
- The head commit hedged README's sudo claim to "unless it has passwordless sudo configured", but left the same claim unhedged in the CLI's own refusal message (
apps/rocm/src/therock.rs,refuse_non_interactive_message) and indocs/manual-testing.md— the branch's own pre-warm is the counterexample that motivated the hedge. Harmless in direction (it steers users to the narrow flag) but it is the third recurrence of the reword-most-sites-miss-one pattern this PR keeps hitting; the contributor rules ask for the claim's wording to be grepped across every surface, not just the one being edited. README.md:209uses an absolute URL to link a heading in the same file while every other same-file anchor uses the relative form. This looks like an inconsistency and is not — it is a deliberate, reproduced fix for a docs build that slices the README into separate pages, so the anchor cannot resolve across the split. Nothing at that line says so, so a future reader or reviewer will "restore consistency" and re-break the build; a one-line comment beside the link naming the reason prevents the recurrence.- Gate coverage in unit tests is all against the pure helpers called with hand-supplied values; nothing pins the gate at its two real call sites, so mutating
existing.is_some()tois_none()in either install path would not fail a unit test. The end-to-end refusal scenario does cover it, but only on a GPU lane. - The
--yes-proceeds scenario is tagged for the nightly lane, which is opt-in and does not run on pull-request pipelines — consistent with existing practice in the same file, but worth knowing that this path's coverage is manual-trigger only. - The wheel path's visible host-newer "Note:" progress string is a hand-written near-duplicate of the pure helper used for the summary line, with no test pinning the progress text; the two can drift apart silently.
siloteemu
left a comment
There was a problem hiding this comment.
✅ Approved on a maintainer's explicit instruction · pr-review-watcher · 34f7eb4
A maintainer reviewed this pull request and instructed that it be approved if it verified clean. It does, so this approval is filed on that instruction. This automation does not approve on its own initiative.
What changes: a consent gate before an SDK install that would displace the active default runtime, refusing non-interactively unless the user opts in explicitly.
Verified at this exact head before approving:
- A published review from this account sits at the current head commit and records no blocking findings; the head has not moved since.
- The pull request is open, not a draft, not conflicting, and mergeable.
- No other reviewer holds an outstanding change request.
- Check conclusions at this head: 24 success, 1 failure, 1 still running (a hardware lane).
About the one failing check. It is the mock end-to-end lane, and it fails on two dashboard throughput scenarios that race the host under load. This is not caused by this change:
- The identical failure — the same two scenarios, the same message — reproduces on the base branch's own tip, on a commit this pull request does not touch.
- That same base commit also passed the same lane on a separate run, which is what a race looks like rather than a deterministic break.
- A change fixing exactly these two scenarios has since landed on the base branch, and the base branch has been green on this lane on every run since. This branch's checks were started before that fix landed, so they do not include it.
- Nothing in this diff touches the code those scenarios exercise.
A re-run of that lane on top of the current base branch is expected to come back green. Merging remains a human decision; nothing has been enqueued or merged here.
Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 2c99788
The approving review already on this pull request was filed on a maintainer's explicit per-PR instruction and is left standing; this automation files no approvals of its own, and this report is not one. The merge decision stays with a human reviewer.
Summary
The new head is a clean, fully automatic merge of the base branch that leaves this PR's authored content byte-for-byte unchanged, and neither the sysinfo major bump nor the base's dash test-infrastructure change collides with it. No blocking findings. Verified: the merge tree is identical to the tree git merge-tree produces from the two parents, the PR's own diff is identical before and after the merge, no conflict markers survive, and cargo check -p rocm-core passes at this head against sysinfo 0.39.6 — the full suite, the e2e suites and a workspace-wide build were not run here. Blocking: 0 · Non-blocking: 2.
🚫 Blocking (must fix before merge)
None.
Non-blocking
- The base's
sysinfo0.34.2→0.39.6 major bump adapted no call sites (that commit touches onlyCargo.toml,Cargo.lock,MANIFEST.md,THIRD_PARTY_NOTICES.txt). This PR's code never reachessysinfo, andcrates/rocm-corecompiles clean locally; the other three consumers (rocm-dash-collectors,rocm-dash-daemon,rocm-dash-tui) rest on the reported green CI rather than on anything checked here. - Dismissed false positive, recorded so it is not re-raised: one investigator read commit
da0999d's long narrative body ("so give the hook the property the scenario always claimed…") as possibly instruction-like. Reading it directly, it is ordinary engineering rationale — reviewer error triggered by an unusually long-form commit message, not misleading code. It will recur on every verbose commit in this repo; the cheap fix is a criterion rather than a repo change: prose is only an injection signal when it directs the reader to act on an external system, not when it is merely verbose.
Scope and evidence
How much was read, and why. Per the re-review scope, this PR's already-reviewed authored content was not re-read from scratch; the effort went into proving the merge changed nothing and introduced nothing.
- Merge reconstruction — clean and automatic, no hand-authored resolution.
prw-base(8557e680) is also the merge base of the two parents, so the base-tip diff and the merge-base diff coincide.git merge-tree --write-tree 34f7eb47 8557e680yields treee628cdce…, which is exactly2c997889^{tree}. The merge therefore carries zero conflict resolution, hand-authored or otherwise, and no file's content matches neither parent. The merge adds nothing of its own:34f7eb47..2c997889is precisely the three base commits' 9 files,8557e680..2c997889is precisely this PR's 13. - PR's authored change unchanged. Diffing the two diffs — the pre-merge authored work (
3edfb693..34f7eb47) against the post-merge result (8557e680..2c997889) — produces zero bytes of difference. Same 13 files, +3106/−104. Nothing added, dropped or silently altered. - Semantic conflicts.
sysinfo: workspace manifest pins0.39, lockfile resolves0.39.6— they agree. The PR's onlyrocm-corechange is wideningdetect_legacy_rocm_summarytopub(crates/rocm-core/src/lib.rs:2960); that function is pure filesystem probing and the file has nosysinforeference. The PR's new caller inapps/rocm/src/therock.rsterminates there. The crate's actualsysinfocall sites (crates/rocm-core/src/disk_space.rs,crates/rocm-core/src/examine.rs) are untouched by both the bump and the PR. Confirmed independently by building the crate. Test infrastructure:tests/e2e-cucumber/tests/e2e.rs, which defines the shared World, is not touched by the base commit; across all 473 step definitions in the post-merge tree there is not one duplicate step string, so no ambiguous-match panic; the base'sROCM_CLI_DASH_TEST_CLOCK_OFFSET_PATHand this PR'sROCM_CLI_PYTHONhave distinct names and distinct consumers; the PR's runtime-setup scenarios are non-interactive and never touch the PTY driver the base changed; neither feature file declares aBackground:or feature-level tag. - Conflict markers. A grep for
<<<<<<</=======/>>>>>>>across the entire post-merge tree returns nothing.
Standing focuses. Tests in this delta: the only test changes are the base's dash.feature / dash_steps.rs / tui_driver.rs, and they would not pass with their production change reverted — the dashboard observation time is held step publishes a hold directive that only the new parsing in crates/rocm-dash-daemon/src/runner.rs understands, and one scenario writes hold 7 specifically to cross the expiry boundary; without it the clock free-runs and both scenarios return to the race they exist to close. This PR contributes no test change in this delta. Merged-in pull request references: the three commits reference pull requests #232, #363 and #412; all three commits are ancestors of the base tip, so each is on the base branch — no claim is made that any has not landed. CI attribution: none made; the reported check state is taken as given and was not re-derived. Prompt injection: none found — nothing in the diff, commit messages, feature files or configuration attempts to direct the reviewer, and no token was requested or echoed. Leak scan of the merge delta: clean; the only hits are the pin-project-internal crate name and amd_smi_* identifiers naming the tool this repository exists to wrap. Bare ticket identifiers appear in commit subjects and are expressly permitted by AGENTS.md.
Summary
Adds a
--yesflag torocm install sdkso an SDK install can proceed non-interactively, and makes displacing the active default ROCm runtime an explicit, opt-in action.rocm install sdkasks before proceeding, and outside an interactive terminal it refuses unless--approve-replacing-active-default(or--yes) is passed — instead of silently taking over as the default.--yesalso continues to approve required system-package installs (e.g. OpenMPI for vLLM) without asking.--approve-replacing-active-defaultgrants only the runtime-displacement consent. ROCm CLI's own non-interactive surfaces pass that instead of--yes, because they spawnrocmwith no terminal and so cannot answer asudopassword prompt.What the gate is keyed on, and why
The gate is keyed on what the runtime config's
active_runtime_key/default_runtime_idactually resolves to — not on the family and channel being installed.That scoping is load-bearing. Activation is global:
finalize_successful_sdk_installactivates whatever finished installing last, with no family or channel guard, sorocm install sdk --family gfx120X-alldisplaces an activegfx110X-allruntime exactly as a same-family upgrade does. An earlier revision of this PR scoped the gate to the same family and channel; that let the cross-family case through with no prompt and no--yeseven in CI, while the CLI printed "No existing ROCm SDK found" on a host that plainly had one.active_default_runtime_relationnow reports the active default regardless of family or channel.This is a real behaviour change: users are now prompted in cases they were not, specifically
--family/--channelinstalls onto a host that already has an active default. That is intended — those installs take over the default. The consent property is only ever strengthened: nothing displaces the active default without an interactive confirmation or--yes. An install with no active default still never prompts (nothing is displaced), and re-installing or upgrading the runtime that is already the active default keeps the confirmation it has today.What the gate actually guards
runtime_keyembeds the resolved version, soinstall_rootand the runtime manifest are version-distinct. An upgrade or downgrade lands in its own directory with its own manifest and the previous install stays on disk — what changes is which runtime is the active default. Only a same-version reinstall reuses the same install root.Every user-facing string states that and only that:
No active ROCm SDK runtime is configured; installing ROCm SDK <v> for family <f>.— it no longer claims no SDK exists anywhere, because the fresh path is now reachable on a host holding a registered-but-unactivated runtime;sdk install: an existing ROCm SDK is the active default runtime/active default runtime: <relation>;--yesclap help,README.md,docs/testing.md— all say "active default", and the help and README explicitly say a different family or channel takes over the default just the same.The relation text has two shapes:
upgrade|downgrade|reinstall from installed <v> (<key>)when the active default is the same family and channel (a version comparison is meaningful), andreplaces active default <v> for family <f> on the <c> channel (<key>)when it is not (comparing versions across unrelated runtimes would invent a relation).rocm update --applyprints the truth nowapply_runtime_updateused to passassume_yes: trueinto the gate, so essentially everyrocm update --applyover an existing runtime printedApproved by --yes: … which becomes the active default runtime.Both halves were false:Command::Updatehas no--yesflag for the user to have passed, andapply_runtime_updateactivates only insideif activate.The bool is replaced by a consent enum that records where the approval came from:
install_sdk_for_updatenow takesactivate_after_installand buildsUpdateApply { activates }, so the line states only what that path will do: with--activateit says the install becomes the active default; without it, it saysThe active default runtime is unchanged; re-run with --activate, or use \rocm runtimes activate`, to switch to it.Neither credits--yes. The update path stays **preapproved** — it is non-interactive by construction and must never reach a prompt — so nothing about--yes` propagation is weakened.Non-interactive callers
Every surface that spawns
rocmwith null stdin passes a consent flag, so the refusal cannot silently break installs from chat, MCP, the dashboard, or CI.Which flag, and why it matters.
--yeshas always carried a second consent: approve required system-package installs, which means runningsudo. It is threaded straight through tomaybe_auto_install_sdk_preferred_engine(paths, finalized, yes)→ensure_openmpi_for_vllm(approved). Injecting--yeson a terminal-less spawn therefore granted an approval that spawn can never honour: on Linux, for a vLLM-preferred family, with OpenMPI absent and neither root nor passwordless sudo,ensure_openmpi_for_vllmstopped taking its early return atmain.rs:7412(print the manual commands, warn, continue) and instead fell through torun_system_package_install_plan, which runssudo …withStdio::inherit()against null stdin. The failure is then escalated to an error — because the caller "asked for" the install — and the?inmaybe_auto_install_sdk_preferred_engineskips the vLLM engine install that previously proceeded. (rocm install sdkstill exits 0:engine_auto_install_failure_is_fatalonly catchesUnusableRuntimeAfterInstall. What is lost is the engine, and on a surface that owns a terminal the sudo prompt reaches/dev/ttywith a TUI in raw mode in front of it.)The two consents are now separate at the argv boundary rather than overloaded onto one bool.
SdkInstallConsents::resolvemaps--yesto both and--approve-replacing-active-defaultto the first only;system_package_install_actionextracts the shared decision out ofensure_openmpi_for_vllmandensure_torch_runtime_depso it is testable without a package manager. A user-typed--yesis unchanged in every respect, including still escalating a failed package install it explicitly approved.These pass
--approve-replacing-active-default:apps/rocmchatinstall sdkarm and its MCPinstall_sdktool-args builder;apps/rocmdMCPinstall_sdktool (build_install_sdk_args) — a second, independent argv builder whose child is spawned with.stdin(Stdio::null()). Real-install path only; the dry-run path returns before the gate, so it stays bare;build_args()— real-install branch only;build_install_args;xtask/src/e2e_prewarm.rsandscripts/therock_sdk_install_test.pykeep--yes— deliberately, and this is the one place the two flags diverge. xtask was previously left bare because the old family-scoped gate could not fire there; under active-default scoping it can (a shared tree pre-warmed forreleaseand then fornightlyreaches that call with a release runtime active, and xtask has no terminal), so it needs a consent flag. But both are provisioning harnesses whose job includes installing the system packages the SDK needs, and both run as root in CI or from a developer's terminal, where a sudo prompt is answerable and a failed package install is worth failing on. The SDK install harness now appends--yesonly on the real-install path, matchingapps/rocmd: a dry run returns before the gate.Consent is preserved rather than bypassed: the chat/MCP paths still go through
ChatRocmCommandAction::Approvalwith--yesvisible in the approved command, androcmd'sinstall_sdkremains inmcp_tool_requires_direct_approvaland is annotateddestructiveHint.Also in this PR: host-ROCm-vs-TheRock version explanation
Alongside the
--yeswork, this PR surfaces a short explanation when the host's legacy/system ROCm is newer than the TheRock ROCm version being installed, so a user does not read a seemingly-older selected version as a regression:host_rocm_version_newer_thanreports the host's ROCm version only when it is strictly newer than the version being installed.version_note:line is added to both the wheel and tarball install summaries, and echoed as a visible progress line on the real-install path.repo_version_without_wheelsdrives awarning:line when the repository's newest version has no installable PyTorch wheels for this Python/platform.7.2.4-98above7.13.0. A lenientparse_host_versionstrips a build suffix, tolerates a two-component report, and yieldsNone— "can't tell", not "newer" — when either side is unparseable.Implementation notes
therock::install_sdkreturnsSdkInstallResult { output, mutated }; callers finalize the runtime only when the install actually mutated state, rather than keying offdry_runalone.active_default_runtime_relationpropagates manifest-directory and config read errors instead of degrading to "no active default" — a read error is precisely when we are least sure what is active, and swallowing it would skip the gate.current_runtime_manifestis nowpub(crate)so the gate resolves the active default the same wayruntimes list,runtimes activateandupdatedo, rather than reimplementing theactive_runtime_key→ unambiguous-default_runtime_idfallback.mutated, so an install declined at the prompt is logged assdk install cancelled ...rather thancompleted.fresh_install_line,preapproved_install_line,refuse_non_interactive_message,active_default_relation_text) so the exact wording is unit-testable.Testing
Gates run on Linux:
cargo test --workspace --all-targetsandcargo clippy --workspace --all-targets -- -D warnings, both clean on this head.Unit tests (each falsified — the covered code was broken, the test confirmed red, then restored):
active_default_relation_gates_a_different_family_or_channel— the regression this revision fixes. Falsified by restoring the family/channel filter.active_default_relation_none_without_an_active_default— no active default, with and without registered manifests, must not prompt. Falsified by adding a newest-manifest fallback.active_default_relation_classifies_upgrade_downgrade_reinstall— same-family relation wording preserved.active_default_relation_propagates_manifest_read_errors— a read error must not degrade to "fresh".sdk_install_approval_only_prompts_when_an_active_default_is_displaced— full matrix overAsk/Preapproved(AssumeYes)/Preapproved(UpdateApply)× terminal. Falsified by making the non-interactiveAskarm proceed.preapproved_install_line_credits_the_real_consent_source— theupdate --applyarms must not credit--yes, and the no---activatearm must not claim the install becomes the active default. Falsified by collapsing the arms onto the--yestext.fresh_install_line_claims_no_absent_sdk— the fresh line must not say "no existing ROCm SDK found". Falsified by restoring that string.update_has_no_yes_flag_to_credit— pins the premise ofUpdateApply:rocm update --helpoffers no--yes. Falsified by adding one.an_injected_consent_does_not_approve_privileged_package_installs— the consent split. It parses the argv a non-interactive surface actually produces and asserts what that argv consents to, rather than which flag string it contains, then pins both halves: the injected consent maps toPrintManualCommandson a host without passwordless sudo, a user-typed--yesstill maps toRunPlan { escalate_failure: true }, and the root/passwordless path still installs with no approval at all. Falsified by folding the narrow flag back intosystem_packages.install_sdk_real_install_args_approve_only_the_runtime_replacement(apps/rocmd) — the second, independent argv builder must carry the narrow flag and never--yes. Falsified by restoring--yes.install_sdk_help_separates_the_two_consents_yes_carries—--helpmust document the narrow flag and say it excludes system-package installs, because a reader who takes the two flags for synonyms reaches for--yesfrom a script and gets a sudo prompt nothing can answer. Falsified by dropping the distinction from the help text.install_sdk_help_describes_the_gate_as_replacing_the_active_default, plus the chat/MCP/dashboard argv tests (updated to assert the resolved consent, not the flag spelling).update_has_no_yes_flag_to_creditis a canary, not a regression test: it pins the premise ofSdkInstallApprovalSource::UpdateApply(thatrocm updateoffers no--yesto credit) and would still pass with the production change reverted. Stated here because that is worth knowing when reading the test list. Every other added test references symbols or strings absent atprw-baseand fails on revert.E2E scenarios in
tests/e2e-cucumber/features/runtime_setup.feature:@id:runtime-install-sdk-overwrite-requires-yes(runtime-11,@requires-gpu) — a reinstall with neither consent flag exits non-zero, and the error names--approve-replacing-active-defaultahead of--yes. Runs on the per-push self-hosted GPU lane.@id:runtime-install-sdk-other-family-requires-yes(runtime-13, new,@requires-gpu) — the newly gated case: installing a different GPU family while a runtime is the active default is refused without either consent flag.runtime-11cannot catch this — it reinstalls the same family, so it passed under the old family-scoped gate too. The thirdThenasserts the error names the active default it would replace, which is what separates a real gate refusal from any other non-zero exit that happens to print a consent flag. No@nightly: it is a refusal, so it costs a resolve, not a multi-GiB install.@id:runtime-install-sdk-overwrite-with-yes(runtime-12,@requires-gpu @nightly) — with--yesthe reinstall proceeds; assertion updated to the newApproved by --yes: an existing ROCm SDK is the active default runtimeline. Exercised by the scheduled Nightly workflow'se2e-gpu-nightlyjob, or aninclude_nightlydispatch ofe2e-selfhosted.yml.@id:runtime-install-sdk-help-separates-consents(runtime-14, new) —rocm install sdk --helpmust offer--approve-replacing-active-defaultand state that it does not approve system-package installs. Help-text coverage through the built binary, which arender_long_help()unit test cannot prove;runtime-11sets that precedent. No runtime state, no GPU, so this one does run on the default GitHub-hosted PR lane.Where each lane covers what:
runtime-14runs on the default GitHub-hosted PR lane (ci.yml); the other three are@requires-gpu, so none of those run there (ci.yml) — there, coverage of the gate is the unit tests above.runtime-11andruntime-13run on the per-push self-hosted GPU lanes (e2e-selfhosted.yml);runtime-12is additionally@nightly.Gap:
cargo xtask e2eandpython scripts/smoke_local.pywere not run for this revision — the Linux build host became unreachable partway through the e2e run and did not return. The full workspace test and clippy gates above did complete on this exact tree. The CI lanes are authoritative for the e2e result.Note on cost: the refusal is not download-free. The gate reports the version relation, so it runs after the Python launcher is resolved and the channel index is read; on a cold host the launcher step can still fetch. What it does bail before is the SDK and torch download and any change on disk.
Merged with main
origin/mainwas merged into this branch (no rebase, no force-push). Conflicts and their resolutions are recorded in the merge commit message. Notably, both sides appended scenarios numberedruntime-08/runtime-09; main's canonical-provenance scenarios keep 08/09 and this branch's--yesscenarios were renumbered (they areruntime-11/runtime-12, the cross-family one isruntime-13, and the help scenario isruntime-14) so indexes stay sequential pertests/e2e-cucumber/tests/feature_naming.rs. The@id:tags are unchanged.Follow-up at
d6435b40Three user-facing strings still credited or recommended
--yeson paths thatnever pass it:
install_sdktook a bareassume_yes: bool, so every install from aterminal-less surface printed
Approved by --yes.SdkInstallApprovalSourcegains anApproveReplacingActiveDefaultvariant,SdkInstallConsentsrecords which flag granted the replacement instead offlattening it to a bool, and
install_sdktakes theSdkInstallConsent.preapproved_install_line_credits_the_real_consent_sourcenow pins the newarm's wording.
refuse_non_interactive_messagetold scripts and CI to re-run with--yes—the flag whose second consent they cannot honour. It now names
--approve-replacing-active-defaultfirst and still explains when--yesisright. New
refusal_recommends_the_narrow_flag_before_yespins the order; thee2e step is now
the error explains how to approve the replacement non-interactivelyand asserts the same ordering, andruntime-11/runtime-13are retitled "without consent".
build_argstest comment still described a--yesit no longerasserts; it now also pins that
--yesis absent.Also, from the non-blocking list: the pre-warm comment now gives the real reason
it keeps
--yes(it wants the system packages, and its runners have passwordlesssudo), the OpenMPI escalation site records that
finish_sdk_installdowngradesthat error via
engine_auto_install_failure_is_fatalsoinstall sdkstillexits 0, and the README synopsis no longer implies the two consent flags are
mutually exclusive.