feat(web): uninstall the agent on device delete, prompt when unreachable (VS-70) #90

Merged
nrupard merged 2 commits from feat/VS-70-delete-device-uninstall-agent into main 2026-06-30 17:31:00 +02:00
Owner

What

Implements server-orchestrated device delete (VS-70). Deleting a device now uninstalls the agent from the machine instead of only dropping the DB record and leaving the agent running + reconnecting.

Behavior

On removedevices (the user control action), per node, after the existing UNINSTALL right check:

  • Agent connected: send the VA-87 control-channel uninstall action and wait up to 10s for the agent's ack. The agent acks BEFORE it tears down (so the server can safely delete), then removes its own unit/binary and exits. On ok ack -> delete the record. Result ok.
  • Agent reachable but uninstall errored: result uninstall-failed, record kept.
  • Agent not connected or ack times out: result agent-unreachable, record kept. The UI shows the "Delete anyway?" prompt and re-issues with top-level force: true.
  • force: true: skip the uninstall, delete the record outright (the still-installed agent must be removed manually). Result ok.

Result tokens are kebab-case to match the handler's existing ok / no-nodes / no-db / not-authorized / db-error. These strings are the contract pinned in the VS-70 issue and consumed by the vervain-apps delete dialog (VAPP-55).

How

The agent control channel had no request/response correlation: every server->agent send (AgentRegistry::send_json) was fire-and-forget, and agent replies were fanned out to user sessions by dispatch_text, never matched to an outstanding request. Added the minimum needed:

  • AgentRegistry gains a pending_uninstall_acks map keyed by requestid. register_uninstall_ack returns a oneshot receiver the delete path awaits with a timeout; resolve_uninstall_ack / cancel_uninstall_ack complete or drop it.
  • agent.rs::dispatch_text intercepts the agent's uninstall reply, resolves the waiter by its responseid, and consumes the frame (it is a server-internal ack, not a user-facing frame).
  • user_session.rs: RemoveDevices gains a force flag; remove_devices gains the uninstall orchestration via a request_agent_uninstall helper; the in-memory agent registry entry is dropped after the record is deleted.

This is additive: the existing fire-and-forget sends and uninstall_agents path are untouched.

Dependencies

  • Agent side (VA-87, merged): the agent handles the uninstall control action, acks {"action":"uninstall","ok":true,"responseid":N}, then fully removes itself.
  • UI side (VAPP-55): consumes agent-unreachable to show the force prompt and re-issues with force: true. Can merge independently; the server tolerates a UI that never sends force (it just never force-deletes).

Verification

In the compose dev image (docker compose -f compose.dev.yml run --rm --no-deps app cargo ...):

  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.
  • cargo test --lib -p vervain-web -> 418 passed, 0 failed. New tests: registry ack round-trip / unknown-id / cancel (agent_state); and remove_devices connected-uninstall-then-delete, unreachable-blocks, reachable-but-failed (uninstall-failed), and forced DB-only delete (user_session).

End-to-end confirmation (a real connected agent self-removing on delete) lands when this deploys to staging and the mokosh-server e2e / a manual delete exercises it.

#VS-70

## What Implements server-orchestrated device delete (VS-70). Deleting a device now uninstalls the agent from the machine instead of only dropping the DB record and leaving the agent running + reconnecting. ## Behavior On `removedevices` (the user control action), per node, after the existing UNINSTALL right check: - Agent connected: send the VA-87 control-channel `uninstall` action and wait up to 10s for the agent's ack. The agent acks BEFORE it tears down (so the server can safely delete), then removes its own unit/binary and exits. On `ok` ack -> delete the record. Result `ok`. - Agent reachable but uninstall errored: result `uninstall-failed`, record kept. - Agent not connected or ack times out: result `agent-unreachable`, record kept. The UI shows the "Delete anyway?" prompt and re-issues with top-level `force: true`. - `force: true`: skip the uninstall, delete the record outright (the still-installed agent must be removed manually). Result `ok`. Result tokens are kebab-case to match the handler's existing `ok` / `no-nodes` / `no-db` / `not-authorized` / `db-error`. These strings are the contract pinned in the VS-70 issue and consumed by the vervain-apps delete dialog (VAPP-55). ## How The agent control channel had no request/response correlation: every server->agent send (`AgentRegistry::send_json`) was fire-and-forget, and agent replies were fanned out to user sessions by `dispatch_text`, never matched to an outstanding request. Added the minimum needed: - `AgentRegistry` gains a `pending_uninstall_acks` map keyed by `requestid`. `register_uninstall_ack` returns a oneshot receiver the delete path awaits with a timeout; `resolve_uninstall_ack` / `cancel_uninstall_ack` complete or drop it. - `agent.rs::dispatch_text` intercepts the agent's `uninstall` reply, resolves the waiter by its `responseid`, and consumes the frame (it is a server-internal ack, not a user-facing frame). - `user_session.rs`: `RemoveDevices` gains a `force` flag; `remove_devices` gains the uninstall orchestration via a `request_agent_uninstall` helper; the in-memory agent registry entry is dropped after the record is deleted. This is additive: the existing fire-and-forget sends and `uninstall_agents` path are untouched. ## Dependencies - Agent side (VA-87, merged): the agent handles the `uninstall` control action, acks `{"action":"uninstall","ok":true,"responseid":N}`, then fully removes itself. - UI side (VAPP-55): consumes `agent-unreachable` to show the force prompt and re-issues with `force: true`. Can merge independently; the server tolerates a UI that never sends `force` (it just never force-deletes). ## Verification In the compose dev image (`docker compose -f compose.dev.yml run --rm --no-deps app cargo ...`): - `cargo fmt --check` clean. - `cargo clippy --all-targets -- -D warnings` clean. - `cargo test --lib -p vervain-web` -> 418 passed, 0 failed. New tests: registry ack round-trip / unknown-id / cancel (agent_state); and `remove_devices` connected-uninstall-then-delete, unreachable-blocks, reachable-but-failed (uninstall-failed), and forced DB-only delete (user_session). End-to-end confirmation (a real connected agent self-removing on delete) lands when this deploys to staging and the mokosh-server e2e / a manual delete exercises it. #VS-70
feat(web): uninstall the agent on device delete, prompt when unreachable (VS-70)
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 2s
dee4fba7cc
Device delete previously removed only the DB record, leaving the agent installed and reconnecting. Orchestrate a real uninstall: on `removedevices`, for a connected agent the server now sends the VA-87 control-channel `uninstall` action and waits for the agent's ack (which the agent emits before it tears down), then deletes the record. The machine is left clean.

When the agent is not connected, or does not ack within a 10s timeout, the delete does NOT proceed: it returns the `agent-unreachable` result so the UI can show the "Delete anyway?" prompt. The UI re-issues with a top-level `force: true`, which skips the uninstall and deletes the record outright (the still-installed agent must then be removed manually). A reachable-but-failed uninstall returns `uninstall-failed` and keeps the record. Result tokens are kebab-case to match the handler's existing `ok` / `no-nodes` / `not-authorized` / `db-error`; these strings are the contract pinned in the issue and consumed by the vervain-apps delete dialog (VAPP-55).

The agent control channel had no ack correlation (every server->agent send was fire-and-forget), so add a minimal pending-ack map to `AgentRegistry` keyed by the `requestid` the server stamps and the agent echoes back as `responseid`: `register_uninstall_ack` hands the delete path a oneshot receiver it awaits with a timeout, and the agent text-frame router in `agent.rs` resolves it (and consumes the `uninstall` reply rather than fanning it out to web clients). The UNINSTALL right is still enforced before any of this, and the in-memory agent registry entry is dropped once the record is gone.

Tests: registry ack round-trip / unknown-id / cancel; and `remove_devices` for the connected-uninstall-then-delete, unreachable-blocks, reachable-but-failed, and forced DB-only paths. Verified in the compose dev image: cargo fmt, clippy --all-targets -D warnings, and the vervain-web lib suite (418 tests) all pass.

#VS-70

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(web): tidy the uninstall-ack helper from VS-70 review
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 55s
Create release / Create release from merged PR (pull_request) Has been skipped
66c502c657
Two review nits, no behavior change:

- `request_agent_uninstall` returned `Result<bool, ()>` where the bool and the unit error each encoded a distinct outcome. Replace with a 3-variant `UninstallOutcome` enum (Done / Failed / Unreachable) so the call site reads by name instead of by `Ok(true)` / `Ok(false)` / `Err(())`.
- The request id was generated by the caller as a bare `rand::random::<u32>()`, leaving a (1/2^32) window where two concurrent deletes could pick the same id and clobber each other's waiter. Move id allocation into `AgentRegistry::register_uninstall_ack`, which now generates the id under the pending-map lock and retries on collision, returning `(id, receiver)`. The id is unique among currently-pending acks by construction.

Verified in the compose dev image: cargo fmt, clippy --all-targets -D warnings, and the vervain-web lib suite (419 tests, +1 for the distinct-id allocation) all pass.

#VS-70

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/VS-70-delete-device-uninstall-agent 2026-06-30 17:31:00 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!90
No description provided.