feat(server,agent): v2 secure-session-core Task 5 - attended consent
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 5m42s
Build and Test / Build Agent (Windows) (push) Successful in 8m22s
Build and Test / Security Audit (push) Successful in 5m12s
Build and Test / Build Summary (push) Has been skipped

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:
2026-05-30 07:44:09 -07:00
parent 8cb0b5b16b
commit 9082e11490
10 changed files with 906 additions and 5 deletions

View File

@@ -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