feat(server,agent): v2 secure-session-core Task 5 - attended consent
SPEC-002 Phase 1 Task 5, code-reviewed APPROVED. An attended (support-code) session is invisible and inert to the technician until the end user accepts a consent prompt on their own machine. - proto: ConsentRequest / ConsentResponse + ConsentAccessMode enum (oneof fields 80/81; no existing field renumbered). - server: ConsentState on Session; attended -> Pending, managed -> NotRequired; join_session refuses viewers unless Granted/NotRequired (single chokepoint - StartStream only fires from join_session, so no frames or input flow pre- consent); run_consent_handshake sends ConsentRequest, 60s timeout, granted -> proceed, denied/timeout/disconnect -> teardown (end_session denied, machine offline, support code released). consent_state persisted; consent_requested/ granted/denied audited. - agent: Windows MessageBox (topmost/system-modal) on spawn_blocking; anything but an explicit Yes = deny; non-Windows build is a fail-closed stub. Not cargo-check-verified locally (no toolchain). Server verified on the build host; the Windows agent half is verified by CI build-agent (Pluto). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -250,10 +250,61 @@ Reference: audit Pass B/E (rate limiting disabled/non-compiling; reusable codes)
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Attended-mode consent
|
||||
## Task 5 [IMPLEMENTED 2026-05-30 — self-reviewed; no Rust toolchain on this machine, not yet `cargo check`-verified; pending Code Review]: Attended-mode consent
|
||||
|
||||
Files touched: `proto/guruconnect.proto`, `agent/src/session/mod.rs`, `agent/src/` (consent UI dialog),
|
||||
`server/src/relay/mod.rs`, `server/src/session/mod.rs`.
|
||||
> [IMPLEMENTED] An ATTENDED (support-code) session now requires the end user to
|
||||
> ACCEPT a native consent prompt before the technician's session is surfaced.
|
||||
>
|
||||
> - PROTO (`proto/guruconnect.proto`): added `ConsentRequest` (server/relay →
|
||||
> agent: `session_id`, `technician_name`, `access_mode` (`ConsentAccessMode`
|
||||
> = `CONSENT_VIEW`|`CONSENT_CONTROL`), `timeout_secs`) and `ConsentResponse`
|
||||
> (agent → server: `session_id`, `granted`, `reason`), inserted AFTER
|
||||
> `AdminCommand`. New `Message` oneof field numbers `consent_request = 80`,
|
||||
> `consent_response = 81` (no existing field renumbered).
|
||||
> - SERVER (`session/mod.rs`): new `ConsentState` enum
|
||||
> (`NotRequired|Pending|Granted|Denied`, `as_db_str`/`allows_viewer`) added to
|
||||
> the in-memory `Session`. `register_agent` starts attended (`!is_persistent`)
|
||||
> sessions `Pending`, managed/persistent `NotRequired`. `join_session` REFUSES
|
||||
> any viewer unless `consent_state.allows_viewer()` (only `Granted`/
|
||||
> `NotRequired`) — this is the gate that keeps a support session invisible to
|
||||
> the technician until accepted. New `set_consent_state`/`get_consent_state`.
|
||||
> Unit tests cover the db-string mapping, the viewer-admission predicate, and
|
||||
> the attended-pending-blocks / granted-admits / managed-admits / denied-blocks
|
||||
> transitions.
|
||||
> - SERVER (`relay/mod.rs`): after registering an attended agent,
|
||||
> `run_consent_handshake` sends `ConsentRequest`, audits `consent_requested`,
|
||||
> then waits up to `CONSENT_TIMEOUT_SECS = 60` for a `ConsentResponse`.
|
||||
> granted → `consent_state = Granted` + audit `consent_granted` + proceed.
|
||||
> denied/timeout/agent-disconnect → `consent_state = Denied` + audit
|
||||
> `consent_denied`, send a Disconnect to the agent, end the session row
|
||||
> (`status='denied'`), release the code, and TEAR DOWN (early return — the
|
||||
> technician never sees the session). In-memory consent is authoritative; the
|
||||
> DB `consent_state` (via `db::sessions::update_consent_state`) is a durable/
|
||||
> audit mirror. A late/dup `ConsentResponse` in the main loop is logged+ignored
|
||||
> (no silent unhandled variant). `db::events` gained `CONSENT_GRANTED`/
|
||||
> `CONSENT_DENIED`/`CONSENT_REQUESTED`. `db::sessions::create_session` now sets
|
||||
> `is_managed`/`source`/`consent_state` from `is_support_session` (attended →
|
||||
> `false`/`standalone`/`pending`; managed → `true`/`gururmm`/`not_required`).
|
||||
> `api::SessionInfo` echoes `consent_state` so the dashboard can show "awaiting
|
||||
> consent".
|
||||
> - AGENT (`consent/mod.rs` [new], `session/mod.rs`, `main.rs`): on
|
||||
> `ConsentRequest`, `handle_consent_request` runs a blocking Windows
|
||||
> `MessageBox` (MB_YESNO | TOPMOST | SETFOREGROUND | SYSTEMMODAL | ICONQUESTION)
|
||||
> on `spawn_blocking` so the async loop/heartbeats are not stalled, phrasing
|
||||
> the prompt VIEW vs VIEW-and-CONTROL from `access_mode`, then sends a
|
||||
> `ConsentResponse`. Anything other than an explicit Yes (closed box, panic) is
|
||||
> a DENY. Non-Windows build is a `// TODO(platform)` stub that fails CLOSED
|
||||
> (denies). Unit tests cover the prompt wording + the access-mode decode
|
||||
> fallback.
|
||||
> - Managed/unattended sessions are `not_required`, never prompted (Phase-1
|
||||
> default). PER-TENANT consent policy beyond this default is a future
|
||||
> refinement — left as a TODO (no per-tenant policy table consulted yet).
|
||||
> - No `.unwrap()` on a non-test path; runtime `sqlx::query`; no code/secret/
|
||||
> token value logged. No Rust toolchain here — self-reviewed only.
|
||||
|
||||
Files touched: `proto/guruconnect.proto`, `agent/src/session/mod.rs`, `agent/src/consent/mod.rs` (new),
|
||||
`agent/src/main.rs`, `server/src/relay/mod.rs`, `server/src/session/mod.rs`, `server/src/db/sessions.rs`,
|
||||
`server/src/db/events.rs`, `server/src/api/mod.rs`.
|
||||
|
||||
- Add `ConsentRequest` / `ConsentResponse` to the proto (after `AdminCommand`).
|
||||
- On an attended session, the agent shows a consent dialog to the end user; the server keeps the session
|
||||
|
||||
Reference in New Issue
Block a user