Repository navigation
Conversation
jussielo-amd
left a comment
There was a problem hiding this comment.
This is careful, well-tested security work. The POSIX single-quote quoter is correct (verified the escaping logic by hand and against the property tests), every preview site the description claims is actually wired to it with no leftover ad hoc quoting, and the control-character guard closes a real gap — rejecting CSI/ESC sequences reaching the terminal, not just NUL/CR/LF.
The test strategy is the standout here: round-tripping through both a POSIX word-splitter and a real /bin/sh (with glob bait and a sentinel $HOME) is exactly what's needed to catch a quoter that looks correct to a naive splitter but still lets $HOME or `id` expand on paste. The two new e2e scenarios are genuinely discriminating against the old quoters, not just plausible-looking. The documented coverage gaps for the chat and dash approval cards are real infrastructure limits (traced both against the mock server and issue #501), backed by solid property/unit coverage rather than hand-waved.
Checked this against the known auto_applicable iGPU/dGPU drift area from #149/#382/#413/#395 — this PR doesn't touch GPU/device-policy code at all, so no overlap there.
One small thing worth a follow-up fix whenever convenient: docs/architecture.md's new line says rocm-dash-tui's copy is ui::exec::quote_arg, but the function is actually named quote_display_arg.
Nice to have, not blocking: a short comment on shell_quote's inert predicate noting that allowing = bare relies on the quoted value never landing in argv[0] of a sh -c string — true at every current call site, just not structurally enforced.
f5b911f to
dc4a255
Compare
|
Rebased onto current
On the review's two notes:
Re-verified on
Reverting each guard or quoter one at a time still turns the matching tests red. |
dc4a255 to
55873c9
Compare
|
Rebased onto main Verified locally on the new head, all exiting 0:
|
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 55873c9
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.
Review — needs work
Full review of the whole change (prw-base...55873c99, 13 commits, 16 files).
The description says the change makes every command preview denote the argv it runs, and it names #443 as fixed. The code does that for the previews. It also adds a control-character guard whose stated completeness does not hold (below).
Blocking
-
[new] The control-character guard does not cover everything the description says it covers: three paths still print model-chosen strings raw.
apps/rocm/src/main.rs:11160,apps/rocm/src/main.rs:11413,apps/rocm/src/main.rs:17720,apps/rocm/src/main.rs:17664-17666,apps/rocm/src/main.rs:17476
The description says "The guard applies to every chat tool whose arguments reach the terminal". The doc onCHAT_TOOLS_REACHING_THE_OPERATORS_TERMINALsays every unlisted tool "constrains the one it takes". That holds for accepted values only; the refusal messages echo the rejected value.- A hostile tool name is echoed by
unsupported ROCm tool{other}``. The guard checks only arguments, and an unlisted name skips it. - A hostile
port_statushost is echoed bynon-local host{host}``.port_statusis not on the list. - A hostile planner
intentortoolis echoed by the planner errors.build_freeform_plan_with_contextthen pushes{error}intodeterministic.notes, and the plan block prints notes verbatim. This bypasses both the new argv refusal and the new notes filter. - The first two errors reach the terminal through
?atmain.rs:10973, then the top-levelError: {e:?}print. The third reaches it through the plan block'snote:line. I traced these paths by reading the code and did not run a binary to the terminal. - I ran probe tests that call the validators directly.
validate_chat_tool_callreturned an error string containing U+001B for both aport_statushost ofx\u{1b}[2Kyand a tool namedbad\u{1b}[2Ktool.provider_planner_response_to_planwith an ESC-bearingintentreturned an error string containing the ESC. - Governing side: the stated guarantee ("the model, not this code, then chooses what the operator reads") governs. The allow-list and the "constrained" comment are what fall short.
Confidence 90 · logic · Fix: refuse or escape control characters incall.name, inport_status'shost, and in the plannerintent/toolbefore they are echoed. Alternatively invert to deny-by-default with an explicit exemption list (natural_language_plan,rocm_command), so a new tool fails safe. If the gaps are meant to stay, narrow the description and the comment to say so.
- A hostile tool name is echoed by
-
[new] The
Arrayand nested-Objectarms ofreject_control_charactersare exercised by no test.apps/rocm/src/main.rs:11260-11275(and the tests atmain.rs:34560-34740)
The doc says the guard "walks the whole arguments object rather than naming fields, so a new argument … inherits the guarantee". No test sends an array or a nested object to a guarded tool, and no guarded tool reads one. I replaced theArrayarm withOk(())and rancargo test -p rocm --bin rocm command_preview_properties: 14 passed, 0 failed.
Confidence 100 · logic · Fix: add cases such asinstall_sdkwith{"prefix":"/x","extra":["a\u001b"]}and with{"extra":{"k":"a\r"}}, each asserting the guard's own wording. The existing per-tool test already asserts that wording. -
[new]
a_windows_install_folder_keeps_its_separatorspasses with the production change reverted.crates/rocm-dash-tui/src/ui/exec.rs:~311-330
The valueC:\Program Files\ROCmcontains a space, so the old quoter also wrapped it, and shlex reads the old double-quoted form back unchanged. I restored the oldquote_display_argbody and ran the exec tests: this test passed while 10 of 15 others failed. The test is also the only Windows-side backslash check, because the real-shproperty test is unix-only. It stays green if\is added to the inert set, since the space forces quoting anyway.
Confidence 100 · logic · Fix: use a space-free backslash path such asC:\ROCm. A bare backslash makes shlex returnC:ROCm, so the test would then fail without the quoting. -
[new]
an_argument_can_never_add_a_word(three copies) claims a guarantee it cannot fail on.crates/rocm-dash-tui/src/ui/exec.rs:~189-198,apps/rocm/src/shell_quote.rs(an_argument_can_never_add_a_word),apps/rocm/src/main.rs(an_argument_can_never_add_a_word_to_the_preview)
The comments say a;,|or>inside a value is "data, never syntax" and "can never read as a second command". shlex splits only on whitespace and quote or escape handling. It treats none of; | & < >as operators.- In
exec.rsI added; | & < >to the inert set.an_argument_can_never_add_a_wordstayed green, and so did the shlex round-trip properties. Onlydisplay_args_quotes_shell_metacharactersand the unix real-shtest failed. - The
apps/rocmcopies use the sameshlexconstruction, so the same holds there. I did not mutate those separately. - Each copy is also implied by its round-trip test.
- A test whose comment claims a branch it cannot fail on is ranked worse than no test, because it stops the next reviewer from looking.
Confidence 100 forexec.rs, 85 for the other two · logic · Fix: narrow the comments to "no argument adds or loses a whitespace-delimited word", or drop the redundant tests and rely on the real-shproperty tests, noting that those are unix-only.
- In
Non-blocking
-
[new]
natural_language_plan's "echoed nowhere" claim is inaccurate for the model-called path.apps/rocm/src/main.rs:11197-11199,main.rs:12674-12695,main.rs:34879(the test and comment)
When the model calls the tool,requestis model-supplied. It is rendered verbatim asrequest:and intotool_call:lines inside the quotes, and returned instructuredContent.text. I confirmed with a probe test that an ESC inrequestsurvives into that text. Which terminal surface printsstructuredContent.textis not settled:rocm chatprints only the fixed summary, and the dash plan review reads it (crates/rocm-dash-tui/src/app/summary.rs:26).
Confidence 70 · logic · Fix: correct the comment, and decide whether the plan text needs the same refusal or a render-boundary scrub. -
[new]
has_terminal_control_characterischar::is_controlonly, so it misses format characters.apps/rocm/src/shell_quote.rs:67-69
U+202A-202E and U+2066-2069 (bidi), zero-width characters and U+2028/2029 pass the guard and land inside'…', where an RLO can reorder the displayed command. That contradicts the "preview denotes the argv" claim.rocm_core::terminal::is_control_or_formatalready exists (crates/rocm-core/src/terminal.rs:129) andapps/rocmdepends onrocm-core.
Confidence 75 · logic · Fix: use that predicate, and add a bidi case to the tests. -
[new] Planner notes are dropped silently, and on the untrimmed string.
apps/rocm/src/main.rs:17608
A benign note ending in\n, or a multi-line one, is dropped with no trace, and the operator gets no signal that anything was omitted.
Confidence 70 · logic · Fix: trim before testing, and add a fixed "N planner note(s) omitted" note. -
[new] The justification for keeping a second quoter copy is false as stated.
crates/rocm-dash-tui/src/ui/exec.rs:41-48,apps/rocm/src/shell_quote.rs:22-30,docs/architecture.md:28, commita0b9f7d
The text says "there is nowhere both can reach".rocm-dash-tui -> rocm-coreis the only forbidden edge (xtask/src/crate_edges.rs).apps/rocmalready depends onrocm-dash-tui, which exposes apubquoter, and both crates depend onrocm-dash-core. The result is three byte-identical quoters, two copies of the hostile alphabet and two real-shharnesses. Nothing ties the copies together, andshell_quote.rs:77-80says "One copy, deliberately" while a second generator now exists.
Confidence 80 · architectural · Fix: share one quoter, or reword the three comments to say the duplication is a choice rather than a constraint. Separately,docs/architecture.mdcallsshell_quote.rsthe quoter for "any command line shown to an operator or handed to a shell". Manyrocm services stop {}style hints remain unquoted and the doc should say which surfaces are covered. -
[new] Generator and tests leave gaps in which characters are exercised.
apps/rocm/src/shell_quote.rs:104-107,exec.rs:153-156
The alphabet has no[ ] { } % , @ +. Making[and]inert would stay green everywhere except the singlepackage[extra]pin intherock.rs.shell_quote.rs:102says the generator has "No control characters", but it contains\t.a_validated_planner_argv_cannot_forge_a_plan_linegoes red under revert only because of that accidental tab.- The approval property test returns early when validation refuses, so it can pass vacuously.
a_copyable_command_survives_being_pastedpins one spelling rather than paste behaviour, and stays green if$becomes inert, because its value contains spaces.
Confidence 70 · mechanical · Fix: add those characters to the alphabet and a fixedpackage[extra]example to the dash tests, and correct the comment.
-
[new] Both new scenarios would pass on the old quoters on a Windows lane whose temp root contains whitespace.
tests/e2e-cucumber/tests/e2e/runtime_steps.rs:1395-1405,tests/e2e-cucumber/tests/e2e/therock_steps.rs:~718-760
The old quoters also wrapped on whitespace, and the lone'does not discriminate once the whole path already has a space. Linux and spaceless roots do discriminate. The therock-next-11 step also matches any word whose file name equalsit's-here, which is looser than the symlink reason requires.
Confidence 55 · mechanical · Fix: assert the root has no whitespace, or compare the full path or itscanonicalize(). -
[new] Coverage gaps under AGENTS.md section 3, beyond what the description already declares. The description declares the chat approval card and the dash approval cards. It does not declare these, which have no scenario:
- the new refusal of a model tool call carrying a control character, which is observable as an error;
- dropped planner notes;
- the
rocm serve '<model>'placeholder text.
I found no scenario asserting any of the three. State why each is unreachable offline, or add a scenario where one can run. The dash preview is separately uncovered by any scenario (dash-12 asserts only the title and the buttons).
Confidence 75 · architectural.
-
[new] The history-narrating comments in
remote/mod.rsshould go.apps/rocm/src/remote/mod.rs:1238-1244and:1427-1430record whereshell_quote"used to" live. That is a changelog in the code. Confidence 70 · mechanical. -
[new] Commit hygiene. Confidence 85 · mechanical.
a0b9f7dhas a truncated paragraph. The dash-TUI sentence starts mid-sentence ("fixes the whitespace case from #443 …"), which looks like a subject line eaten by comment stripping.829aba5and5cd2c1eare rustfmt-only commits, so earlier commits are notfmt-clean and not bisectable.6ae6989,093049d,55873c9andf7d5744fix earlier commits of the same PR.a0b9f7dis about 1,500 lines and bundles two logical changes: the quoting fix, and a new control-character refusal across nine tools,rocm_command, planner argv and planner notes. AGENTS.md section 11 asks for one logical change per PR. A split would also give the guard its own description.
-
[new]
quoter_propertiesreach test.shell_quote.rs:204-213and theexec.rstwin create arocm-…-glob-bait-<pid>directory in the temp dir and never remove it. One directory leaks per test process. Confidence 40, so it is below the reporting bar and listed for information only.
Decisions for the author
-
POSIX single quotes on every platform, including Windows copy surfaces — tradeoff
Both sides: for a reader the preview is now unambiguous, and the old double-quoted form was ambiguous everywhere. The description andshell_quote.rs:34-42already disclose thatcmd.exedoes not strip single quotes and that PowerShell fails on a value containing'. The documented Windows shell is PowerShell (README.md:96,docs/manual-testing.md:10), so only the lone-'case andcmd.exepastes break.
Two further points: the dry-runcommand: uv … && uv …line already needed PowerShell 7 orcmd.exefor&&, and the Windows e2e lanes read the line back with POSIXshlex, so scenarios pass on a line a PowerShell user could not paste for the lone-'folder.
rocm_core::runtime_is_windows()andcomfyui.rs:680'spowershell_quoteexist as precedents for a platform-aware renderer. Confirm POSIX-only is the intended scope. -
Refuse at validation versus scrub at the render boundary — tradeoff
Refusing is right for values that become argv, because the approval preview must equal what runs. It does not cover read-only tool output, whichmain.rs:10990-10993prints raw line by line. That includesrocm_commandoutput such as service logs. It also does not cover model prose (main.rs:9818-9820). Both predate this PR. The description's "the model chooses what the operator reads" framing therefore describes only the argument surface. Decide whether a single render-boundary scrub, usingrocm_core::terminal, is the follow-up.
Positive signals
- Moving the remote quoter into one module and asking a real
/bin/shto read the rendered line back is the right oracle. The shlex-only version is shown, in the PR's own comment, to pass against the defect. generator_reaches_the_shapes_that_break_quotingmeasures the generator's reach and fails below a floor, so the alphabet cannot be tuned into harmlessness unnoticed.every_guarded_tool_refuses_a_control_character_in_its_rendered_argumentasserts the guard's own wording rather thanis_err(). Several tools would refuse the value for another reason, so a looser assertion would stay green with the guard deleted.- The control-character cases pick a folder named with a lone
'rather than a space. They discriminate old from new on Linux, which a space would not. - The Windows lanes and the real shell are both exercised by name through
TEST_SH = "/bin/sh", avoiding aPATHrace with other tests in the binary.
Deployment notes
- Copyable and printed command lines now use POSIX single quotes. Windows paths render as
'D:\ROCm\…', where they rendered bare before. Thecmd.exeand PowerShell limits are described under Decisions. - Model tool calls to guarded tools,
rocm_command, and planner argv that carry any Unicode control character are now refused with an error. Previously only NUL, LF and CR were refused onrocm_command, and only NUL on planner argv. The error text is new and undocumented indocs/llm-tool-use.md(that doc was not made false, per AGENTS.md section 7). shlexbecomes a dev-dependency in three crates.shlex 2.0.1was already inCargo.lock, so no new package enters the lockfile.
What this covered
Every changed file was read completely in its changed regions, plus the surrounding code. That includes apps/rocm/src/shell_quote.rs, remote/mod.rs, therock.rs, the main.rs validators, renderers and handlers, crates/rocm-dash-tui/src/ui/exec.rs and its callers, the e2e features and steps, the Cargo manifests, and docs/architecture.md. Read against prw-base (7a062ad7) to 55873c99. Six read-only Sonnet workers each covered one coupling group. A seventh question group covered the design questions: Windows rendering, completeness of the one-quoter claim, and guard placement. Findings were consolidated and verified by the driver.
Runs by the driver (one build at a time, one shared target dir, since removed):
cargo test -p rocm --bin rocm command_preview_propertieswith theArrayarm ofreject_control_charactersstubbed toOk(()): 14 passed.cargo test -p rocm-dash-tui --lib exec::with the old double-quotequote_display_argrestored: the Windows-folder test passed and 10 of 15 others failed.cargo test -p rocm-dash-tui --lib exec::with; | & < >added to the inert set:an_argument_can_never_add_a_wordand the shlex properties passed;display_args_quotes_shell_metacharactersand the real-shproperty failed.- Probe tests, since reverted:
port_statusand unknown-tool refusal strings, a plannerintenterror, andnatural_language_planecho each carried the ESC byte. - I did not run the full workspace test suite, clippy or the e2e lanes locally.
CI at head 55873c99 (live statusCheckRollup): every check that reported passed. That covers clippy, build-and-test, windows-build-and-test, Test (affected crates), Commit signatures + sign-off, E2E tests, the Strix Halo Ubuntu, Windows and WSL2 lanes, prek, Coverage (rocm-dash crates, ratcheted) and CodeQL. Regenerate manifests, Skill checks and Sphinx docs build were skipped. E2E tests (MI300X) was still queued, so it had not reported.
No check runs rustdoc, so cargo doc was not run. Possible rustdoc ambiguity on the [`shell_quote`] link at main.rs:17918 (both a module and a function) is unsettled, confidence about 60, so it is not scored.
The description and #443 were read at Step 9. The description matches the diff except for the guard-completeness claim and the "pinned per entry" claim under Blocking. #443 is OPEN, and the change is about it. The discussion on the PR was not read, and this report is not reconciled against it. Commit signature status was not checked locally, because %G? does not reflect the API's verified state here.
Did not run: the prior-changes pass (it needs earlier review comments, which this run may not read). History found nothing that the change undoes.
55873c9 to
7850393
Compare
|
Rebased onto main
Verified locally on the new head, all exiting 0:
|
7850393 to
a1872ef
Compare
|
Rebased onto main Since this branch was cut, #604 added the shared
Ordinary ids print unchanged. New tests split each hint and both remote stop commands back to their argv. Removing the quoting from the builder fails Older hints that predate this branch still print values bare:
I'll take those in a follow-up. Verified locally:
|
a1872ef to
855392f
Compare
|
Thanks. I verified every finding before fixing it. They are addressed in Blocking
Non-blocking
Not in this round:
Decisions
Verified locally:
The one local failure was the known spinner timing flake, which passed on re-run. |
This reviewed 55873c9. The head is now 855392f: dc25c5b shares one shell quoter, and 855392f makes the chat-tool guard deny-by-default (no refusal echoes the refused value), tests the nested Array/Object arms, fixes the Windows separator test and drops the tests that could not fail. All required checks are green on 855392f.
855392f to
be9a10b
Compare
|
Rebased onto main
Local gates, all green:
|
be9a10b to
2a228d9
Compare
|
Rebased onto
Local checks all pass:
|
2a228d9 to
263f8b7
Compare
|
Rebased onto |
|
Rebased onto main
Merged with current main: |
263f8b7 to
c2764cb
Compare
|
Rebased onto main |
c2764cb to
a27e68f
Compare
The chat approval card, the `request plan` block and the `run:` lines the CLI offers to be copied were all rendered by a quoter that wrapped a value only when it contained whitespace, and even then escaped just `\` and `"`. So a lone `"` left the line unparseable, and `$HOME` in a copyable `rocm chat --prompt …` expanded in the reader's own shell when they pasted it. The operator was being asked to consent to a line that did not denote the argv behind it. There was already a correct POSIX quoter for the remote command line, with tests that ask a real `sh`. Move it to `shell_quote`, use it for the previews and for the install dry-run's `command: uv … && uv …` line, and check all of them with property tests: a rendered argv must split back — through a POSIX word-splitter — to exactly that argv, and no argument may add a word to the line. Arguments are drawn from a small mostly-metacharacter alphabet, because the shapes that break quoting are the ones nobody writes an example for; a test measures and prints that reach so the generator cannot be tuned into harmlessness. The dry-run line's test asks a real `sh` rather than the word-splitter. Written with `shlex` first, it passed against the defect it exists to catch: a splitter hands back ``/tmp/`id` `` unchanged and reports a clean round-trip for precisely the value that executes. fixes the whitespace case from #443 but keeps `$`, `` ` `` and a trailing `\` live inside the quotes. Its `quote_display_arg` now uses the same POSIX single-quote rule, so the dash and the CLI render a value the same way; its call sites and API are unchanged. It is a second copy rather than a shared one because `xtask`'s crate-edge contract forbids `rocm-dash-tui -> rocm-core`, and `apps/rocm` owns `rocm-core`. Quoting cannot make a terminal control character safe — `'…'` is literal to a shell, not to the terminal about to act on a `\r` — so model-supplied arguments carrying one are refused before they reach a printed block. The criterion is "reaches the terminal", not "becomes a command line": that is why `path_exists` is guarded, whose `path` is echoed into an ungated tool-result block that can print alongside a mutating call's rendered command. The guarded list is the guard, so it carries a case per name, written out rather than looped over the constant, with the two tied together by a set-equality test. `rocm_command` keeps its own screening (it trims, so a shared check would disagree with it), widened from naming `\0`/`\n`/`\r` to every control character — ESC and the 8-bit CSI went straight into the approval card. The planner's `notes`, which are printed verbatim into the plan block, now drop any note carrying a control character (including `\n` and `\t`) for the same reason. `natural_language_plan` is deliberately unguarded: its `request` is the user's prose, echoed nowhere, and legitimately spans lines. Scope note: apart from planner notes, this covers model-supplied arguments, not model prose. Assistant reply text is still printed as it comes. Platform note: the quoting is POSIX everywhere. The previous double-quoted form happened to paste cleanly into `cmd.exe` and `'…'` does not. A value without a `'` reads back unchanged in bash, WSL and PowerShell; PowerShell spells an embedded `'` as `''` rather than `'\''`, so a value containing one is unambiguous to a reader there but not paste-exact. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The round-trip property tests used shlex as their only oracle. shlex splits words and removes quotes but expands nothing, so it hands back `~`, `?`, `$HOME` and a backtick unchanged and reports a clean round-trip for a quoter that leaves them bare. Adding `~` or `?` to the inert set left the whole suite green, although both change the argv once a shell reads the line. Add a property test that hands the rendered line to a real `sh`, in a directory with files for `?` and `*` to match and with HOME at a sentinel, and demands the argv back. With `~` or `?` made inert it now fails on the one-character input. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The dash quoter's only oracle was shlex, which expands nothing: adding `$` or `~` to its inert set left every dash test green, although a preview showing a bare `$$` or `~` no longer denotes the argv the job is spawned with. Add the same real-`sh` round-trip property the CLI-side quoter now has. Bring the test alphabet level with the CLI side's (`=`, `~`, `!`, `é`), so the `=` inert branch is exercised here too, and correct the doc comment that said a change to one copy the other does not make shows up as a failure: nothing ties the two copies together. Each is held to the same contract by its own tests, so either one breaking fails its own suite. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The module doc said the POSIX form is correct in PowerShell. PowerShell does read '…' literally, but it escapes an embedded single quote by doubling it, so the '\'' idiom this quoter emits for a value containing ' does not round-trip there. Say so, rather than claiming parity. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…alsifiable The planner-note filter added with the control-character guard had no test: deleting it left the suite green. Add one that feeds the provider planner a note carrying a CSI sequence and one carrying a carriage return, and requires both to be gone and the ordinary note to survive. The handler half of a_read_only_path_probe_cannot_redraw_the_block_it_prints_into asserted `!any(is_control) || !contains(ESC)` on the output for `/tmp/my folder`, which no handler change could make false. Assert the premise the guard rests on instead — the handler echoes the model's path verbatim — and say that in the doc comment, which claimed more. Also point the scope note at `strip_ansi_sequences` where it lives (`comfyui.rs`, private); it is not in `rocm_core`. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Scenario runtime-20 drives `rocm --yes <request>` with an install folder whose name carries a lone `'`, and reads the request plan's and the execution section's `rocm install sdk` tool_call lines back through a POSIX word-splitter, requiring `--prefix` to be followed by exactly that folder. The previous quoter wrapped only on whitespace, so it rendered that folder bare and left a line no shell can parse; with it restored, the scenario fails on the request plan step. A space alone would not distinguish the two quoters on this surface. Reuses runtime-15's Given, so the install stops on its first step: offline, instant, and the same on every lane. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Scenario therock-next-11 previews a wheel install with `--prefix` naming a folder whose name carries a lone `'`, and reads the dry-run's `command: uv … && uv …` line back through a POSIX word-splitter, requiring one word to name that folder. With the previous whitespace-only quoter restored the folder renders bare and the scenario fails. Reuses therock-next-01's loopback pip-index fixture, so it needs no network, GPU or gate. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…omponents So the next site that renders a command line for an operator finds the shared quoter rather than writing another one, and knows why the dash TUI carries its own copy. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The real-`sh` round-trip tests spawned `"sh"`, which is looked up on `PATH` at spawn time. `therock.rs`'s `render_update_json` test narrows the process-global `PATH` to a stub directory while it runs, so under `cargo test` — one process for every test — a spawn landing in that window failed with "sh should run: No such file or directory". Seen locally on `shell_quote::tests::a_real_shell_reads_a_rendered_argv_back_unchanged`. CI's per-process runner hides it; a local run does not. Spawn `/bin/sh` instead, in the four tests this change adds or moves. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Both copies of the quoter leave `=` unquoted. That is sound only because no caller renders a value in command position, where `A=b` is an assignment; nothing enforces it, so say so at the predicate. Also name zsh's `=word` expansion, which sits outside the POSIX contract. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The `rocm services <action> <id> --yes` hints now come from one builder, and runtime activation prints `rocm services logs <id>` and `rocm runtimes activate <key> --restart-services --yes` lines of its own. All of them interpolated the value bare, so an id with a space or a quote named a different argv than the one the user would paste. Quote the id once in the hint builders, so every caller gets it, and quote the id and key in the activation report. The remote stop hint now quotes for its callers rather than trusting each to pre-quote; it is the same line whether it is shown or sent to the remote shell. An ordinary id or key is inert and prints unchanged. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The CLI's `shell_quote` and the dash's `quote_display_arg` were
byte-identical, and the comments justifying two copies said there was
nowhere both could reach. There is: `apps/rocm` already depends on
`rocm-dash-tui`, so `shell_quote` now delegates to the dash's quoter
and the quoter's own tests (round-trip, real `sh`) live only beside it.
The edge that is forbidden is `rocm-dash-tui -> rocm-core`, which this
does not need.
The quoting tests are sharpened where a review showed they could not
fail on what they claimed:
- the Windows-folder test used `C:\Program Files\ROCm`, whose space
made the old double-quoted form pass too; it now uses `C:\ROCm`,
which a POSIX reader turns into `C:ROCm` unless the value is quoted;
- `an_argument_can_never_add_a_word` (three copies) claimed `;`, `|`
and `>` stay data, but `shlex` treats none of them as operators, so
it was implied by the round-trip test and checked nothing more; the
copies are dropped and the round-trip doc says which claim only the
unix real-`sh` property checks;
- the generator alphabet gains `[ ] { } % , @ +`, the fixed shapes
gain `package[extra]`, and the comment no longer says the alphabet
has no control characters (it has `\t`, as whitespace).
`docs/architecture.md` now names the surfaces the quoter covers rather
than claiming every command line the CLI prints, and `remote/mod.rs`
loses comments narrating where the quoter used to live.
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The control-character guard was an allow-list of nine tools, and its completeness held only for values that were accepted. Refusals echoed the value they refused: an unsupported tool name, a `port_status` host outside loopback, and the provider planner's `intent` and `tool`, the last two into the plan block's notes. Each reached the terminal raw. - The guard is deny-by-default. Every tool's whole arguments object is screened before anything else reads it, except `rocm_command`, which screens its argv itself after trimming. A tool added later fails safe. - A tool name carrying a control character is refused first, by a message that does not repeat it. - The planner's refusals do not echo a hostile `intent` or `tool`, and the note every planner failure becomes is stripped of control sequences, since it can carry provider text from any error path. - A planner note is trimmed before it is tested, and dropped notes are counted in a note of their own instead of vanishing. - `natural_language_plan`'s `request` is screened too. It is echoed into the plan text the tool returns, not nowhere as the comment said; the user's own `/plan` input comes from a single-line field. - The predicate is `rocm_core::terminal::is_control_or_format` plus U+2028/U+2029, so a bidi override, a zero-width character or a line separator is refused like an escape sequence. Tests assert the guard's own wording, and that no refusal carries the character, for each path; the array and nested-object arms of the walk get cases of their own. The planner-forge property now draws control-bearing arguments on purpose instead of reaching the guard only through the alphabet's tab. `docs/llm-tool-use.md` states the rule. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
#546 added two pieces of ComfyUI advice that name a command to run as printed: `rocm comfyui install --runtime-id <key>` after a reinstall is interrupted, and after an install fails once the new code is in place. Both interpolated the runtime key bare, the shape this PR removes from every other line the CLI offers to be copied. Build the command once, quoting the key with the shared quoter, and use it in both messages. A generated key is inert and prints unchanged. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
`has_terminal_control_character` spelled U+2028/U+2029 out in its own `matches!`, a second copy of the set `rocm_core::terminal:: is_control_or_line_separator` already owns for exactly this reason. Call that instead. Behaviour is unchanged (both predicates include `Cc`), and the non-ASCII char escapes the ASCII-output contract flags are gone. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The quit-confirmation popup added on main meanwhile prints
`rocm services stop {id} --yes` for each model still serving, both from
the registry-only startup fallback and beside a known instance, with the
id bare. An id that is not one inert word then pastes back as several
arguments. Quote it with `quote_display_arg`, as every other hint this
change covers; an ordinary id prints unchanged.
Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
#535's checker lists the service hint's format string as not-an-invocation. This branch now quotes the id there, so the string reads 'rocm services {action} {} --yes'; the exclusion follows it. The architecture note no longer spells the free-form request as a command. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
a27e68f to
c5c148b
Compare
|
Rebased onto main
Verified locally:
|
nowycondro
left a comment
There was a problem hiding this comment.
Automated review (medium/high-effort recall pass): 7 findings across correctness (two spots where this PR's own "quote the hint" fix wasn't applied to sibling occurrences of the identical string in the same file), architecture (four parallel hand-maintained control-character checks instead of one general gate; a silent-strip path inconsistent with the rest of the PR's refuse-don't-mangle design), and minor efficiency/simplification cleanups. See inline comments.
| if !call.arguments.is_object() { | ||
| bail!("ROCm tool `{}` arguments must be a JSON object", call.name); | ||
| } | ||
| reject_control_characters_in_tool_arguments(&call.name, &call.arguments)?; |
There was a problem hiding this comment.
Four independent, hand-written "no control characters reach a terminal" checks instead of one general gate.
This call (reject_control_characters_in_tool_arguments) is one of four separately-maintained enforcement points this PR adds/widens for the same invariant:
- this walk, for most chat tools;
normalized_chat_rocm_command_args's own inlinehas_terminal_control_character(arg)check forrocm_command's argv;validate_provider_planner_tool_call's own.any(...)check for the planner's argv;provider_planner_response_to_plan's inline loop for planner notes (which strips and prints viaplanner_failure_note/strip_terminal_control_sequencesrather than refusing, unlike the other three).
None of the four is reachable by construction from a new call site — a future tool, a new planner field, or a new printed-preview path can be added without hitting any of them, which is exactly the "a list of exceptions fails safe when forgotten" risk this PR's own doc comment calls out for mechanism #1 specifically (CHAT_TOOLS_SCREENING_THEIR_OWN_ARGUMENTS is deny-by-default; the other three are not). A single choke point — e.g. sanitizing/refusing model- and provider-supplied strings once at deserialization, or routing all four through one shared validator — would not depend on every future call site remembering to opt in.
Related: planner_failure_note (main.rs) silently strips rather than refuses, which is also an inconsistency with the rest of this PR's "refuse, don't mangle" design — a hostile provider error (e.g. a forged "approval: not required" line) is silently cleaned and printed with no indication anything was altered.
| /// | ||
| /// [`crate::shell_quote::shell_quote`] is the same quoter the remote command | ||
| /// line and the approval previews use. | ||
| fn quote_display_arg(value: &str) -> String { |
There was a problem hiding this comment.
Minor simplification: this quote_display_arg is now a pure 1-line wrapper around crate::shell_quote::shell_quote(value), with only two call sites (both in the same function, both inside .map(|arg| quote_display_arg(arg))). Consider deleting the wrapper and calling crate::shell_quote::shell_quote directly at both call sites — one less indirection for a reader to chase.
| /// came from the provider — an HTTP failure body, or a value its response | ||
| /// named. The planner's own refusals do not echo a hostile value, but this is | ||
| /// the one place every failure passes through, so it is stripped here too. | ||
| fn planner_failure_note(provider: &str, error: &anyhow::Error) -> String { |
There was a problem hiding this comment.
planner_failure_note strips control characters (rocm_core::terminal::strip_terminal_control_sequences) and silently prints the mangled result, unlike every other path this PR adds, which refuses on a control character (or, for planner notes a few lines below in provider_planner_response_to_plan, drops-and-counts rather than silently mangling in place). A hostile provider error containing a forged line (e.g. a fake "approval: not required") gets silently cleaned and printed with no indication it was altered — reintroducing the "operator can't tell original content from altered content" ambiguity that refusing (the pattern used everywhere else in this PR) is meant to eliminate.
| /// Walks the whole arguments object rather than naming fields, so a new | ||
| /// argument on one of these tools inherits the guarantee instead of having to | ||
| /// remember it. | ||
| fn reject_control_characters_in_tool_arguments( |
There was a problem hiding this comment.
Efficiency note: this recursive control-character walk now runs twice on the same unchanged call in at least two places that were already calling validate_chat_tool_call back-to-back before this PR, but which this PR makes each call more expensive (a full recursive JSON walk instead of the old cheap per-arg \0/\n/\r check):
apps/rocm/src/dash_seam.rs'sBinToolExecutor::executecallschat::validate_chat_tool_call(&call)explicitly (line 37), then for a non-read-only tool callschat::chat_tool_approval_request(&call, None)(line 46), whose first line re-runsvalidate_chat_tool_callon the same call.deterministic_mutating_tool_call_for_promptvalidates a call, and the caller that wants its title (deterministic_mutating_tool_intro) immediately callschat_tool_approval_request, which re-validates and rebuildsargs/display_commandviashell_quote, only to readapproval.pending_title— the rest of the built request is discarded immediately, and the same call is validated/rendered again later for the real approval card.
Not a new bug, but this PR increases the cost of each of these now-redundant passes. Worth a cheap guard (e.g., a validated: bool the caller can skip, or reading the title without building the full approval request) if this path is latency-sensitive.
| let _ = writeln!( | ||
| output, | ||
| "After this, `rocm services logs {service_id}` and `{}` no longer work.", | ||
| "After this, `rocm services logs {}` and `{}` no longer work.", |
There was a problem hiding this comment.
This PR fixes the exact wording `rocm services logs {service_id}` here (wrapping it in shell_quote), but two sibling occurrences of the identical string elsewhere in this same file are left unquoted:
write_past_attempts_hint:writeln!(output, " Read the newest: rocm services logs {id}")(~main.rs:6610)render_services_text:writeln!(output, " logs: rocm services logs {}", record.service_id)(~main.rs:6671)
ManagedServiceRecord.service_id/newest_id are plain Strings with no inert-character guarantee once persisted, so an id with a space/quote/metacharacter makes these two lines split into multiple words on paste — exactly the defect class this PR closes everywhere else, missed two call sites away from where the identical string was just fixed.
Separately (lower confidence, possibly already covered by this PR's stated follow-up list): "Next step: rocm runtimes activate {}", manifest.runtime_key (~main.rs:2148) and " next step: rocm runtimes activate {}", installed.runtime_key (~main.rs:7710) have the same unquoted shape as the rocm runtimes activate <key> --restart-services --yes hint this PR did quote in runtime_services.rs. The PR description's "not covered by a scenario" section lists "runtimes activate" among older hints deferred as follow-up, so this may be intentional — flagging in case it wasn't meant to include these two.
Minor, unrelated: has_terminal_control_character (apps/rocm/src/shell_quote.rs) ORs rocm_core::terminal::is_control_or_format(c) and is_control_or_line_separator(c), both of which independently call c.is_control() first — for the common case (an ordinary character), this re-evaluates is_control() twice per character on what is now a hot path (every chat-tool argument string, walked recursively). Could be a single combined predicate in rocm_core::terminal doing one is_control() check.
|
🤖 Automated review (Claude Code) · c5c148b Summary review pass at head Findings (most severe first):
No blocking correctness regressions found at this head. Cross-file caller tracing for What I read / could not verify: Reviewed the diff at head 🤖 Generated with Claude Code |
nowycondro
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · c5c148b
Review — major concerns
Full review of the whole change: the three-dot diff between origin/main at d59f596f6e08297c207f008021eecb37defc3558 and the PR head c5c148b5801f3e5b53d15e5e0d86475b20839fd8 (23 files, +1961/-139), covering all changed files completely.
Implements ROCMAI-530 ("Command previews shown for approval do not denote the argv actually run," status In Progress) — the PR's described scope matches the ticket, and also names GitHub issue #443 ("Fixes #443") and builds on merged PR #500, consistent with what the diff does.
Blocking
-
The PR currently has unresolved git conflicts with
mainand is forked from a stale point that predates a landed, overlapping refactor — repo/branch state (not a single line), corroborating evidence atapps/rocm/src/shell_quote.rs:63-64,crates/rocm-core/src/quoting.rs:92-100.
Verified directly:gh pr view 563 --repo ROCm/rocm-cli --json mergeable,mergeStateStatusreturns{"mergeable":"CONFLICTING","mergeStateStatus":"DIRTY"}. The PR's merge-base withorigin/mainis1c33248c(2026-10-23), five days stale;origin/mainsince gained commitfe1dbabe("ROCMAI-727: consolidate shell/PowerShell quoting into rocm-core (#743)"), which (a) extracted large portions ofapps/rocm/src/main.rsintoapps/rocm/src/freeform_planner.rsand sibling modules — confirmedfreeform_planner.rsexists onorigin/mainbutgit show pr/563:apps/rocm/src/freeform_planner.rsfails ("exists on disk, but not in 'pr/563'"), while this PR edits the same planner functions in place insidemain.rs— and (b) addedrocm_core::posix_shell_quote, whose body (crates/rocm-core/src/quoting.rs:92-100) is now byte-identical torocm_dash_tui::ui::exec::quote_display_arg, the function this PR's own newapps/rocm/src/shell_quote.rs:63-64delegates to.rocm_core::quoting's own module doc explicitly says it deliberately leavesquote_display_argunmerged because it "uses a materially different, currently-inconsistent escaping convention" — a premise this PR makes false without ever seeing that doc, because the two branches never met.
Confidence 100 · architectural · Fix: rebase onto currentmain, re-home the edited planner logic intofreeform_planner.rs(and any other extracted module this PR still edits inmain.rs), and decide whetherapps/rocm/src/shell_quote.rsshould delegate to the now-identicalrocm_core::posix_shell_quoteinstead of (or in addition to)rocm-dash-tui's copy. -
CI check "E2E tests (MI300X, current)" is failing on the reviewed head commit —
gh pr checks 563 --repo ROCm/rocm-cli(re-verified immediately before this report).
The job log could not be inspected (gh run view --log-failed→403 Forbidden, self-hosted-runner logs not accessible to this account), so whether this is a regression from this change or an unrelated/infra flake on the self-hosted MI300X lane is not established either way — stated as a gap rather than assumed safe.
Confidence 100 that the check is red · architectural · Fix: investigate the job, and if it is a known flake, re-run it or note that explicitly on the PR; a red check should not be left unexplained.
Non-blocking
-
comfyui.rs'srun_uv_logged_commandstill writes its"command: {uv} {args}"log line through a pre-existing, un-migrated quoter —apps/rocm/src/comfyui.rs:2298-2307(quote_log_arg, unchanged by this diff — confirmed viagit diff ...pr/563 -- apps/rocm/src/comfyui.rs | grep quote_log_arg, no hits) and its call site atapps/rocm/src/comfyui.rs:2065-2073.
quote_log_argfalls back to Rust's{value:?}Debug formatting for anything non-trivial: Debug doubles backslashes, so a Windows install path such asD:\ROCm\therock_venvs(quoted correctly elsewhere in this very PR) would render here as"D:\\ROCm\\therock_venvs", a different, nonexistent path if pasted; Debug also never neutralizes$/backtick, so a value containing$(...)stays live inside the double quotes it produces. This is the identical bug classtherock.rs's near-identical dry-run"command: uv … && uv …"line explicitly fixes in this same PR.
Pre-existing and untouched by the diff, so not blocking by itself; non-blocking here because it's a diagnostic log line rather than an interactive approval-gate preview, and its exposure is unchanged by this PR. Flagged because of its direct pattern relevance to this PR's own stated purpose.
Confidence 85 · logic · Fix: migratequote_log_argto the sharedshell_quotein a follow-up. -
remote/mod.rs's copy-pasteablessh {target} -- …hint lines still interpolatetargetbare, inconsistent with the rest of the same file —apps/rocm/src/remote/mod.rs:318, 422, 455, 600, 1120, 1144, 1229, 1237(all pre-existing, untouched by this diff's 55/58-line change to this same file).
targetis documented elsewhere in this file as stored "exactly as the user typed it" (unlikesession_id, which this PR did migrate toshell_quotein these very call sites). Practical exploitability is tempered becausetargetmust resolve to a live Tailscale peer name first, which in practice constrains it to DNS-safe characters — but the inconsistency sits inside a file this PR reworked for exactly this purpose, with no comment explaining whytargetwas left out.
Confidence 70 · logic · Fix: either quotetargettoo, or add a one-line comment (matching the style already used elsewhere in this PR for other intentionally-bare values) explaining why it's safe to leave bare.
Decisions for the author
-
Delegate
shell_quotetorocm_core::posix_shell_quoteinstead ofrocm-dash-tui's copy, once rebased — tradeoff.
apps/rocmalready depends on bothrocm-coreandrocm-dash-tui(apps/rocm/Cargo.tomllists both). Today's choice to delegate torocm-dash-tuiis justified by the crate-edge rule (rocm-dash-tuimay not depend onrocm-core) and is well-documented inshell_quote.rs's module doc, but that reasoning predatesrocm_core::posix_shell_quoteexisting at all. Once rebased, both delegation targets are equally available and implement the identical algorithm; keepingrocm-dash-tui's copy as the canonical one is defensible (it already carries the round-trip/real-shtests), but routing throughrocm_coreinstead would leave only two independently-tested copies (the unavoidablerocm-dash-tuione, plus whichever crate(s) cannot reach it) instead of the current three-way split. Neither option is wrong; worth a deliberate call once the conflict is resolved. -
A hand-maintained exemption list for self-screening tools — non-blocking-improvement.
CHAT_TOOLS_SCREENING_THEIR_OWN_ARGUMENTSinapps/rocm/src/chat.rshas exactly one entry (rocm_command) and is correctly pinned by a test today, so a tool added here without equivalent self-screening would currently be caught. A compile-time or lint-level check tied to the tool registry itself would be more robust than a runtime test as this list grows, given how security-relevant it is. -
Automation-proposal
title/messageare echoed into printed tool-result text without going through the control-character guard —apps/rocm/src/chat.rs:3574, 3607, unverified/open.
proposal_action's own argument screening coversproposal_id/actiononly; whether a proposal's storedtitle/messagecan ever be authored under model influence (vs. always system/user text) could not be traced from the files in this diff — proposal creation lives outside it. If it can be model-influenced, this is the same class of gap the guard elsewhere closes; if not, it's worth a one-line note recording that the scope decision was deliberate. -
Control-character guard uses a blocklist (reject any Unicode Cc/format codepoint, including a bare
\t) rather than an allowlist — tradeoff.
Safe-by-default and consistent with the guard's stated purpose, but could someday reject a legitimate value (e.g. a path containing a literal tab). Worth confirming this was weighed deliberately rather than merely convenient.
Positive signals
- The round-trip property tests go beyond word-splitter checks: they hand the rendered line to a real
/bin/shin a directory seeded with glob bait and a sentinelHOME, and one test case is a literal"x'; rm -rf ~; echo '"injection payload — this proves inertness under actual shell semantics, not just under a parser that performs no expansion. validate_chat_tool_call's control-character screening runs before the "is this tool even supported" check, so an unrecognized or future tool is screened by default rather than silently admitted — genuine fail-closed design, not merely a convention that happens to hold today.- The two new e2e scenarios deliberately use a folder name containing a literal
'rather than a plain space, with an in-feature-file comment explaining that a space alone would already have passed under the pre-PR quoter — well-reasoned choice of adversarial test data rather than an accidental one.
Deployment notes
- This PR cannot currently be merged as-is; see Blocking above (git conflicts with
main). - Intentional, user-visible format change: previews now render
'...'(POSIX single quote) instead of the old"...". This reads back correctly on bash/WSL/PowerShell (modulo a value containing'on PowerShell) but is not paste-runnable as-is incmd.exe, which does not strip single quotes — called out by the PR itself, repeated here for whoever writes release notes or support docs.
What this covered
Every file in the diff, read completely, via five coupling-grouped workers (shell_quote.rs + Cargo.toml/Cargo.lock + advised_commands.rs + therock.rs; chat.rs + docs/llm-tool-use.md; main.rs + remote/mod.rs + runtime_services.rs + service_hints.rs + comfyui.rs; rocm-dash-tui's ui/exec.rs + ui/mod.rs + wave0_job_bridge.rs + Cargo.toml; the e2e-cucumber feature/step files + Cargo.toml + docs/architecture.md), plus two source-reading passes (agent-instruction adherence against the repo's AGENTS.md; combined history + prior-changes-against-merged-PR-#500 + code-comment compliance). d59f596f6e08297c207f008021eecb37defc3558…c5c148b5801f3e5b53d15e5e0d86475b20839fd8.
One worker could not read apps/rocm/src/main.rs (~21,400 lines) byte-for-byte in a single pass; it read the full diff plus an exhaustive pattern-sweep of every command/preview-construction site with full surrounding context, and disclosed this as thorough-but-targeted rather than exhaustive-line-by-line for the file's untouched remainder.
I independently re-verified the highest-impact candidates against the source rather than taking worker reports at face value: the PR's live mergeable/mergeStateStatus, the live CI check list, the byte-identical bodies of rocm_core::posix_shell_quote and rocm_dash_tui::ui::exec::quote_display_arg, the absence of freeform_planner.rs from the PR's tree, and that both comfyui.rs's quote_log_arg and remote/mod.rs's bare target interpolations are untouched by this diff.
Not reconciled against any discussion on PR #563 — its review threads and comments were not read, per the scope given for this review. Ticket ROCMAI-530 was retrieved by identifier (title/status/type only, no comments).
Summary
#500 fixed the whitespace case of #443 in the dash approval previews by wrapping such values in double quotes. This PR finishes the job. It covers the CLI-side previews #500 did not touch, and it changes the dash quoter so its output reads back as the exact argv in a shell, not only for values containing spaces.
A preview is what the operator approves, and some previews are offered to be copied and run. So the rendered line has to split back, in a POSIX shell, to exactly the argv the CLI runs. Before this change that held only for values with no shell-significant characters.
CLI previews (not touched by #500)
format_structured_tool_callrenders the chat approval card, theadvanced manual command:line, thetool_call:lines inrocm <request>'s request plan, and the copyableAfter the server is ready, run:line. It quoted a value only when it contained whitespace, and then used double quotes:"produced a line no shell can parse;$HOMEin a copyablerocm chat --prompt …expanded in the reader's own shell when pasted.therock::quote_display_argrenders the install dry-run'scommand: uv … && uv …line. It left/tmp/`id`completely bare and kept$(id)live inside its double quotes. Both executedidwhen the line was pasted. The install root comes from a--prefixthe local assistant can choose, becauseinstall_sdk_dry_runis a model-callable tool.apps/rocm/src/shell_quote.rs.Dash previews (#500's helper)
ui::exec::quote_display_argkeeps its name, its API and all of fix(dash-tui): quote whitespace in approval-preview args #500's call sites. Its body changes from "double-quote on whitespace and a short metacharacter list" to the same POSIX single-quote rule. Values that were still mis-rendered:$HOMEand`id`(left bare);\(escapes its own closing quote);';#, which turns the rest of the line into a comment;*,?and~.TestBackendsnapshot tests now expect'…'.xtask's crate-edge contract forbidsrocm-dash-tui -> rocm-core. Each copy is checked against a realshby its own tests.Control characters
'…'is literal to a shell, but not to the terminal that receives a\ror a CSI sequence. Model-supplied arguments carrying one are now refused before they reach a printed block.path_exists, whosepathis echoed into an ungated block. The guarded list is pinned per entry by a test.rocm_commandwidens its own check from\0/\n/\rto every control character. ESC and the 8-bit CSI previously reached the approval card.notes, which are printed verbatim into the plan block, are dropped if they carry a control character, including\nand\t.Behaviour change to be aware of
Previews now read
'C:\Program Files\ROCm'rather than"C:\Program Files\ROCm":';cmd.exedoes not strip single quotes, so a preview pasted there is no longer runnable as-is.On every platform the preview is now unambiguous to a reader, which is what an approval gate needs. The old double-quoted form was ambiguous everywhere.
A model-supplied path containing a tab is now refused by the guarded tools.
Tests
Property tests (proptest):
shlex) to exactly that argv;Generator reach: arguments are drawn from a small alphabet that is mostly metacharacters. A test measures how often the generator reaches quotes, metacharacters, whitespace, empty arguments and backslashes, and fails below a floor.
Real-
shround trips:shlexperforms no expansion, so it calls$HOMEor~left bare a clean round-trip. A property test on each quoter therefore hands the rendered line to/bin/shand compares the wordsprintfreceives. The shell runs in a directory with glob bait and a sentinelHOME. The dry-runcommand:test asks a realshfor the same reason.Revert check: with the old quoters and the old control-character checks restored, 20
rocmtests fail. One example:minimal failing input: args = ["\""], rendered asrocm ", which does not parse. The new dash tests run against fix(dash-tui): quote whitespace in approval-preview args #500's quoter fail 10 of 15. One example:preview = "rocm #"splits back to["rocm"], so the argument vanishes.e2e (AGENTS.md §3): two new mock-lane scenarios, both offline and untagged:
@id:runtime-freeform-prefix-with-quote-is-one-argument(runtime-19): a natural-language install into a folder namedit's-hereprintstool_call:lines that read back with that folder as one argument.@id:therock-next-11-dry-run-command-keeps-a-quoted-folder-whole(therock-next-11): the dry-runcommand:line keeps that folder as one word.Both fail with the old quoters restored. The folder name carries a quote rather than a space because the old CLI quoter already wrapped whitespace, so a space would not distinguish old from new on this surface.
Not covered by a scenario, and why:
tool_calls, so no scenario can reach the approval card. Unit and property tests cover the renderer.TestBackendsnapshot tests cover the rendered widgets.Local verification
cargo test -p rocm --all-targetscargo test -p rocm-core --libcargo test -p rocm-dash-tui --all-targets --no-fail-fastcargo clippy --locked --workspace --all-targets -- -D warningscargo clippy --locked -p e2e-cucumber --test e2e -- -D warningscargo fmt --all --checkcargo xtask manifest --checkcargo xtask tpn --checkcargo test -p e2e-cucumber --test feature_namingrocm-dash-tuirun fails only in 3agent::clientstests. They cannot create a token cache under a read-only$HOMEin my environment, and they are unrelated to this change. The 7wave0_job_bridgetests, including fix(dash-tui): quote whitespace in approval-preview args #500's snapshots, pass.cargo xtask e2e -- -n 'runtime-|therock-next-|dash-': runtime-19 and therock-next-11 pass. The scenarios that fail locally need a detectable GPU family, live downloads, or a writable$HOMEon this WSL host without a GPU, and none of them asserts preview text.Dependencies
proptestuses rocm-core's existing spec (default-features = false, features = ["std"]).shlex2 is a dev-dependency. It was already inCargo.lock, so there are no new crates and MANIFEST.md and THIRD_PARTY_NOTICES.txt are unchanged.Fixes #443
Since the review of
55873c99apps/rocm'sshell_quotedelegates toui::exec::quote_display_arg, sinceapps/rocmalready depends onrocm-dash-tui. The quoter's round-trip and real-shtests live beside it.rocm_command, which screens its own argv.natural_language_planrequest containing a newline, and any argument value containing bidi or zero-width characters, is now refused.tool_calls.rocm serve '<model>'placeholder is reachable offline but not yet covered; that is a follow-up.rocm logs <id>,runtimes activate,remote doctor,enable-provider) are a follow-up.