§1: in-chat approval modal + core mutating tools (install/engine/serve/services) - #32
Conversation
rominf
left a comment
There was a problem hiding this comment.
Approve with minor comments. Reviewed this as the security-critical PR and the gate holds: the approval seam is enforced centrally — the LLM tool loop can only return ApprovalRequired, execution happens solely via execute_approved from the event-loop Approve verdict, and both paths re-validate before running. No bypass, no TOCTOU on the validated args, argv-vector spawning (no shell injection). Excellent test coverage proving approval-is-required from both entrypoints.
Minors:
launch_serverarg-building pushes positional user values as bare argv; a value like--managedwould be parsed as a flag by the child clap parser (arg-confusion, not injection — same gated binary). A--separator before positionals would be good defense-in-depth (pre-existing, low risk).approval.rsdefaults the modal cursor toApprove, so Enter on a freshly-opened destructive modal approves. The two-key flow mitigates it, but defaulting destructive modals to Deny is the safer convention.
4d900ab to
09c172d
Compare
c32a45f to
15633bf
Compare
rominf
left a comment
There was a problem hiding this comment.
Requesting changes on signing grounds — this is separate from the code review above, which still stands.
Every commit on this PR is unsigned: GitHub reports verification.verified = false (reason unsigned) for all of them. Signed commits are required before this can merge.
To fix, configure commit signing and re-sign the branch history, e.g. with SSH signing:
git config gpg.format ssh
git config user.signingkey <your-signing-key.pub>
git config commit.gpgsign true
git rebase --exec 'git commit --amend --no-edit -S' <base-branch>
git push --force-with-lease
Since this is a stacked PR, re-signing is easiest from the bottom of the stack upward — re-signing a base branch rewrites its SHAs and the dependent branches will need rebasing/re-pushing on top.
Once the commits show as Verified I'll clear this.
09c172d to
ecb4c53
Compare
15633bf to
73fdb58
Compare
rominf
left a comment
There was a problem hiding this comment.
Signing gate cleared — all commits on this PR now show Verified (verification.verified = true, reason valid). Re-reviewed the current diff against this PR's own base after the re-sign; it's substantively unchanged from my prior review.
Approving on both grounds: code is sound and commits are signed.
ecb4c53 to
0a23116
Compare
0a23116 to
29eefc5
Compare
73fdb58 to
8c50a09
Compare
There was a problem hiding this comment.
Pull request overview
Adds an explicit human-approval gate for mutating ROCm actions invoked from Dash chat/slash flows, wiring the approval modal through the app loop and providing a safe “execute approved” seam that runs via captured subprocess output (to avoid TUI corruption).
Changes:
- Introduces approval intent plumbing +
execute_approvedacross the dash↔bin tool-exec seam, plus app-loop modal routing and result follow-up turn behavior. - Adds slash-command routing for both read-only executor-backed commands and approval-gated mutating commands (
install/engine/serve/services), including concise summarization of tool results. - Registers read-only and mutating ROCm tools in the Rig agent clients, with mutating tools surfacing approval requests to the UI instead of executing in the tool loop.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/dash-parity-map.md | Adds a Phase/coverage parity map for legacy slash commands and their Dash surfaces/tests. |
| crates/rocm-dash-tui/src/ui/tabs/instances.rs | Updates test helpers to include newly added AppState fields. |
| crates/rocm-dash-tui/src/ui/mod.rs | Renders the approval modal last so it overlays all other UI surfaces. |
| crates/rocm-dash-tui/src/tool_exec.rs | Extends the seam with an approval intent payload and an execute_approved method. |
| crates/rocm-dash-tui/src/client.rs | Adds client message variants for slash-tool replies and approval request/result messaging. |
| crates/rocm-dash-tui/src/app.rs | Implements slash-command handling, approval modal flow, executor-backed slash execution, and concise tool result summaries + tests. |
| crates/rocm-dash-tui/src/agent.rs | Registers read-only and mutating ROCm tools; mutating tools surface approvals to the app via a channel + tests. |
| apps/rocm/src/main.rs | Accepts doctor as an alias for examine and adds a guard test ensuring dash read-tool names are accepted by the bin. |
| apps/rocm/src/dash_seam.rs | Implements execute_approved in the bin seam and updates approval intent payload to carry (name, arguments) for replay. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| RocmToolOutcome::ApprovalRequired(intent) => { | ||
| if let Some(tx) = self.approval_tx.as_ref() { | ||
| let _ = tx.send(ClientMsg::ChatApprovalRequired { intent }); | ||
| } | ||
| Ok(json!({ | ||
| "status": "surfaced_for_approval", | ||
| "note": "This action needs operator approval; it has been \ | ||
| surfaced to the operator. Do not retry.", | ||
| })) | ||
| } |
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "channel": { "type": "string", "description": "Release channel: 'release' or 'nightly'." }, | ||
| "format": { "type": "string", "description": "Artifact format: 'wheel' or 'tarball'." }, | ||
| "prefix": { "type": "string", "description": "Install folder (required; never a system path)." }, | ||
| "version": { "type": "string", "description": "Optional explicit wheel version selector." } | ||
| } | ||
| } |
| | clear | A | covered (Phase 3) | `/clear` slash → empties `chat` transcript | `slash_clear_empties_transcript` | | ||
| | quit | A | covered (Phase 3) | `/quit` slash → `should_quit` (event loop breaks) | `slash_quit_sets_should_quit` | | ||
| | exit | A | covered (Phase 3) | `/exit` slash → `should_quit` (event loop breaks) | `slash_exit_sets_should_quit` | | ||
| | doctor | B | covered (Phase 3) | `/doctor` slash → opens the doctor overlay (`doctor_manager`); the read-only `doctor` tool is covered separately via the LLM tool-call seam | `slash_doctor_opens_overlay` / `read_only_tool_round_trips_to_json` | |
| | logs | B | covered (Phase 3) | `/logs` slash → opens the logs overlay (`logs_view`); the read-only `service_logs` tool is covered separately via the LLM tool-call seam | `slash_logs_opens_overlay` / `read_only_tool_round_trips_to_json` | | ||
| | gpu | B | covered (Phase 3) | `/gpu` slash → `ActiveTab::Hardware`; tool `gpu_snapshot` via seam | `slash_gpu_switches_to_hardware` | | ||
| | daemon | B | covered (Phase 3) | `/daemon` slash → `slash_tool` `rocm_command ["daemon","status"]` (off-thread) | `slash_daemon_raises_executor_request` | | ||
| | install | D | covered (Phase 4) | `/install` slash → `install_sdk` mutating tool → approval modal → `execute_approved` (captured subprocess); also LLM tool-call seam | `slash_install_raises_install_sdk_request` / `approve_path_runs_execute_approved` | |
…e/services) Gate the four core mutating ROCm tools behind an in-chat approval modal. Mutating tools are NOT executed inside the rig tool loop: the executor seam returns an ApprovalRequired descriptor, the rig tool surfaces it to the app over a ClientMsg channel, and the action runs only after the operator approves the modal — via the bin's captured-subprocess path (TUI-safe, no stdout corruption). The safety validators stay the single gate (re-validated on the approved replay), so an unsafe call (public bind / non-loopback / CPU device) fails to an error instead of opening the modal. Seam (tool_exec.rs + dash_seam.rs): - ApprovalIntent now carries name + arguments (replayable payload) plus the human-readable title/body. - Re-add RocmToolExecutor::execute_approved; BinToolExecutor runs it via run_internal_mcp_call(name, args, allow_mutation=true). Dash (agent.rs / client.rs / app.rs / ui): - rocm_mutating_tool! macro + 4 tools (install_sdk, install_engine, launch_server, stop_server) that post ClientMsg::ChatApprovalRequired and return a "surfaced for approval" note (no execution); approval_tx threaded through both clients. agent.rs stays the sole rig namer. - PendingApproval + approval modal flow: highest-priority key arm owns focus; Approve replays execute_approved off-thread (spawn_blocking) → concise result turn + exactly one automatic follow-up agent turn (chat_dispatch consumed once, no loop); Deny/Cancel → declined turn, no execution. - Slash commands /install /engine /serve /services unified through the slash_tool path (now handles all three RocmToolOutcome variants, opening the modal on ApprovalRequired). Parity map: install/engine/serve/services D-core rows → covered (Phase 4). Tests (offline): approve drives execute_approved with expected args; deny runs nothing + declined turn; exactly-one follow-up (no self-loop); validator rejects public-bind/CPU before approval; slash dispatch for all four; mutating rig tool surfaces approval without executing.
…, honest /services restart, single-approval guard FIX A: /install slash now requires a prefix arg (mirrors /engine, /serve); bare /install shows usage error and does not dispatch — the validator REQUIRES a prefix, so the modal previously never opened. FIX B: close_overlays now clears self.approval (was leaving a stale approval modal / focus trap). FIX C: /services restart is guided (not yet wired through the chat seam) instead of silently running stop_server — the modal must never say 'restart' while executing 'stop'. Took the ELSE path: properly wiring restart_server exceeds the contained threshold (4+ bin functions in main.rs + agent.rs macro/array/register). FIX D: open_approval guards against clobbering — a second pending approval is discarded with an error turn so the operator never approves args they did not see. FIX E: PendingApproval doc reworded (struct is not Rust-generic).
8c50a09 to
724a76d
Compare
Stack 4/10 · base:
supergoal/phase-3-readonly-and-navThe defining capability: mutating ROCm actions behind explicit human approval.
ui/approval.rs; mutating tools surfaceApprovalRequiredviaClientMsg::ChatApprovalRequired→ app-loop modal → on Approveexecute_approved(runs as a captured subprocess, no TUI corruption) → exactly one follow-up turn.Reviewed: rust + security + maintainability — approval gate is unbypassable. All cargo gates green.