Security follow-ups (audit 2026-05-30, both reviewed APPROVE):
- MEDIUM: viewer tokens were never blacklisted on logout, so a minted
session-scoped viewer token stayed valid up to its 5-min TTL after the user
logged out. Add a per-user ViewerTokenRegistry (Arc<Mutex<HashMap<sub,
Vec<(token, expires_at)>>>>, prune-on-insert) on AppState; mint_viewer_token
registers each token under the user sub; logout drains take_for_user(sub) and
blacklists each via the existing token_blacklist. The viewer WS already calls
is_revoked, so no WS change. Key chain user.user_id == ViewerClaims.sub ==
registry key verified consistent. 8 new tests.
- LOW: relay chat logs now emit content length, not the chat body (support-chat
can carry secrets/PII).
cargo fmt/clippy(-D warnings)/test green on GURU-5070 (37 agent + 61 server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the secure-session-core Tasks 3-5 code review (APPROVE-WITH-FIXES):
- MEDIUM-2: delete the dead `validate_agent_key` "accept-any-key" placeholder +
its AuthenticatedAgent/AuthState scaffolding (zero callers; the real agent
auth is validate_agent_api_key + per-agent cak_ keys). Removes an auth landmine.
- LOW-3: stop interpolating support-code values into 3 relay log lines (bearer
credentials).
- LOW-1: document the X-Real-IP trust requirement in ip_extract.rs (NPM must set
it from $remote_addr); behavior unchanged.
- LOW-2: correct the consent/heartbeat comment in agent session loop (the loop
awaits the dialog; safe because CONSENT_TIMEOUT 60s < HEARTBEAT_TIMEOUT 90s).
cargo fmt/clippy(-D warnings)/test all green on GURU-5070 (89 tests, 0 warnings).
MEDIUM-1 (viewer-token logout revocation) remains a tracked follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
connect_machines.tags is text[] nullable with no default; the derived
FromRow decoded it as non-Option Vec<String>, so rows with NULL tags
threw "unexpected null" - breaking managed-session reconcile on startup
and the authed Machines list. Hit in production on the v2 cutover.
- Replace the derived FromRow on Machine with a manual impl that decodes
every nullable-non-Option column as Option<T> with unwrap_or_default
(tags, is_elevated, is_persistent, status, timestamps), fixing all six
read sites at once. Public field types unchanged.
- migrations/007: backfill NULL tags to empty array, set DEFAULT '{}',
set NOT NULL (no writer inserts NULL: upsert omits tags, metadata
update binds a non-null array). Idempotent with the prod hot-patch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Axum now serves the v2 React/Vite dashboard SPA at / with a client-side
routing fallback, and the dead v1 HTML portal is removed (nothing was
live on the server to preserve).
- SPA served from server/static/app via ServeDir with a fallback to
index.html, so deep links (/machines, /sessions) resolve to the SPA.
- /api/*rest and /ws/*rest return JSON 404 so unrouted API/WS paths never
leak index.html to clients; real /api, /ws, /health, /metrics, and the
/downloads nest keep precedence (matchit static-over-wildcard).
- Path-aware Cache-Control: hashed /assets immutable, index.html no-cache.
- Vite builds to server/static/app (base /); the artifact is gitignored
and rebuilt at deploy time (npm ci && npm run build).
- Removed v1 portal files (login/dashboard/users/index/viewer .html) and
their dead serve_* handlers; the SPA owns /, /login, /dashboard, /users.
Verified locally: server boots, / and deep links serve the SPA, unknown
/api path returns JSON 404 (not HTML), /health and /downloads intact.
cargo build + clippy -D warnings green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPEC-002 Phase 1 Task 7 (the last), code-reviewed APPROVED, locally verified
(cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 89 pass + build).
- Encoder trait + factory: RawEncoder (salvaged, UNCHANGED) and H264Encoder,
selected by negotiation; factory falls back to raw on H.264 init failure.
- Negotiation: agent advertises supports_h264 (MFTEnumEx HW probe, cached) in
AgentStatus; server picks the codec via select_video_codec(supports, prefer)
and stamps StartStream.video_codec; agent re-guards on local HW. Policy
constant DEFAULT_PREFER_H264 = false, so RAW is negotiated for every session
today - H.264 stays dormant until live hardware validation (Task 8).
- MF H.264 encoder (h264.rs, FIRST-CUT / compile-verified-only): HW encoder MFT,
BGRA->NV12 (color.rs, unit-tested), sync drain, fall-back-to-raw on any failure.
- Viewer H.264 decoder (decoder.rs, FIRST-CUT): MF decoder on a dedicated COM
thread; drops+logs on failure, raw render path untouched.
- proto additive: VideoCodec enum, StartStream.video_codec=3,
SessionResponse.video_codec=5, AgentStatus.supports_h264=11.
- Raw+Zstd path byte-for-byte unchanged; remains the guaranteed default/fallback.
Review confirmed unsafe impl Send for H264Encoder is sound (single-owned &mut on
the block_on thread; session future never spawned) and every MF failure degrades
to raw. H.264 is NOT claimed functional - compile/clippy/build-verified only;
live validation + force-IDR + the no-spawn-invariant doc are Task 8 go-live gates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9082e11 compiles + passes all 50 server tests on the build host; only blocked
CI on cargo fmt (4 files) and one clippy -D dead-code denial:
- cargo fmt --all (relay/mod.rs, session/mod.rs, agent consent/mod.rs + session/mod.rs)
- #[cfg_attr(not(test), allow(dead_code))] on session::get_consent_state (a
read accessor currently exercised only by tests)
No logic change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
5d5cd26 compiles + passes clippy -D warnings + all 45 tests on the build host;
only cargo fmt --check failed on one reflowed method chain in ip_extract.rs.
No logic change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves coord todo 3c1f372a (Task-4 review SHOULD-FIX). Behind NPM-on-loopback,
ConnectInfo was 127.0.0.1 so the rate limiter + lockout bucketed every client
under one IP. New shared utils::ip_extract::client_ip() honors X-Real-IP /
X-Forwarded-For (rightmost-untrusted hop) ONLY when the TCP peer is a configured
trusted proxy (CONNECT_TRUSTED_PROXIES env, default loopback, fail-closed);
untrusted peers are keyed by their true peer IP (forged headers ignored). Wired
into the 3 rate-limit middleware, the validate_code lockout feed, and the agent/
viewer WS handlers so the limiter, lockout, and audit ip_address all key on the
real client consistently. 13 unit tests (spoof rejection, XFF walk, fail-safe
defaults). Code-reviewed APPROVED. Not cargo-check-verified locally (no toolchain);
build-host/CI verification follows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4 (bfcdbb5) compiles and passes all 32 tests on the build host; only
clippy -D warnings blocked CI. Fixed the two denials:
- rate_limit.rs: converted a dangling /// doc block (no documented item) to //
to clear clippy::empty_line_after_doc_comments
- db/events.rs: #[allow(dead_code)] on CONNECTION_REJECTED_EXPIRED_CODE and
_CANCELLED_CODE (not-yet-wired audit-event constants), matching the file's
existing STREAMING_STOPPED pattern; TODO comments note the rejection-event wiring
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPEC-002 Phase 1 Task 4 (the final keystone task), code-reviewed APPROVED.
Closes the audit's reusable-code HIGH and rate-limiting-disabled HIGH.
- Rebuilt rate limiting as a self-contained in-memory per-IP limiter (replaces
the non-compiling tower_governor; removed that dep). Fixed-window caps wired
to login (8/min), change-password (5/min), code-validate (15/min) -> 429;
per-IP lockout after 10 consecutive failed code validations (15-min cooldown).
- Single-use support codes: atomic consume on first agent bind (in-memory
Pending->Connected under write lock + DB conditional UPDATE), rejecting a
second presenter; validate/preview does not consume.
- Widened code format: XXX-XXX-XXX, 31-char unambiguous alphabet (no 0/O/1/I/L),
CSPRNG + rejection sampling, ~44.6 bits (replaces 6-digit numeric); migration
006 widens the code columns to TEXT.
Completes the keystone (Tasks 1-4): every audit CRITICAL + HIGH in the secure
auth/session core is now addressed. Known follow-up todos (not blocking): (1)
trusted-proxy client-IP extraction (NPM-on-loopback collapses clients to
127.0.0.1); (2) multi-instance fail-closed DB single-use gate. Not
cargo-check-verified locally - build-host/CI verification follows this commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Task 2/3/authz commits failed CI at the first gate (cargo fmt --all
--check), which short-circuited before clippy/build/test ran. Verified on the
build host (172.16.3.30): the v2 server compiles and all 18 tests pass; only
3 cosmetic issues blocked CI, all fixed here:
- cargo fmt --all (whitespace, 3 files)
- clippy unused_imports: drop ViewerClaims from auth/mod.rs re-export
- clippy doc_overindented_list_items: de-indent one doc line in sessions.rs
Testing Agent confirmed fmt + clippy -D warnings + build --release + test are
all green with these applied. No logic changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Authz-strength fix (coord todo c8916c89), code-reviewed APPROVED. Replaces the
weak "view" gate (held by every role) with a permission-tiered access mode
stamped inside the signed viewer token:
- mint: is_admin() || has_permission("control") -> CONTROL token; else
has_permission("view") -> VIEW_ONLY token; else 403.
- enforce: the relay drops MouseEvent/KeyEvent/SpecialKey for a VIEW_ONLY token
before forwarding (video still streams); CONTROL tokens forward under the
Task-3 throttle. Mode is unforgeable (in the signature) and unbypassable
(all other viewer->agent payloads hit the catch-all and are never forwarded).
A low-privilege viewer-role user can now at most watch, never control. New
ViewerAccess enum (view_only|control) on ViewerClaims; 3 unit tests.
Audit CRITICAL #1 now fully closed (mechanism in Task 3 + this authz strength).
Not cargo-check-verified locally (no toolchain) - the push triggers CI
(clippy -D warnings + build + test) which is the verification gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPEC-002 Phase 1 Task 3 (specs/v2-secure-session-core), code-reviewed APPROVED.
- viewer_ws_handler: verify the session-scoped VIEWER token (validate_viewer_token
sig+exp+purpose) + token_blacklist.is_revoked + session_id claim == requested
session, before upgrade. Raw login JWTs no longer accepted on the viewer plane
(closes audit CRITICAL #2; closes the *mechanism* of CRITICAL #1).
- mint_viewer_token: authz gate is_admin() || has_permission("view") -> 403.
- Agent identity binding: validate_agent_api_key returns AgentKeyAuth; a cak_-
verified agent rebinds to the key's machine identity (fails closed if
unresolvable), so a key for machine X cannot seize machine Y's session slot.
- Frame caps on both WS upgrades (agent 4 MiB, viewer 64 KiB) - closes WS-OOM HIGH.
- Viewer->agent input throttle (200 ev/s token bucket, bounded try_send) - closes
input-injection MEDIUM.
- Startup managed-session reconcile clarified.
KNOWN FOLLOW-UPS (tracked todos): (1) authz STRENGTH - the "view" permission is
held by every default role incl. viewer, and a viewer token grants input control,
so the gate should be "control" or a VIEW_ONLY/CONTROL token split; CRITICAL #1 is
mechanism-closed, strength pending decision. (2) revoke minted viewer tokens on
logout (currently bounded only by 5-min TTL). Not cargo-check-verified (no toolchain
on the authoring host).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPEC-002 Phase 1 Task 2 (specs/v2-secure-session-core), code-reviewed APPROVED.
- DELETE the JWT-as-agent-key branch in relay validate_agent_api_key (audit
CRITICAL): agent auth now = per-agent cak_ key (SHA-256 -> connect_agent_keys,
revoked filtered) OR support code OR deprecated shared AGENT_API_KEY (warned).
A user JWT can no longer authenticate an agent.
- auth/agent_keys.rs: cak_ gen (OsRng 256-bit) + SHA-256 hash + verify.
- auth/jwt.rs: ViewerClaims + create/validate_viewer_token (5-min TTL,
purpose=viewer, session_id+tenant_id claims; non-interchangeable with login).
- Admin key issuance: POST/GET/DELETE /api/machines/:agent_id/keys.
- POST /api/sessions/:id/viewer-token mints a session-bound short-lived token.
- Migration 005: organization/site/tags on connect_machines (fixes the silent
update_machine_metadata write, coord todo faf39fe0).
NOTE: viewer-token minting is gated by AuthenticatedUser only; the AUTHORIZATION
check (admin/permission gate) that closes audit CRITICAL #1 lands in Task 3 (the
viewer WS verification). The viewer WS path (relay/mod.rs:285) is untouched here.
Not cargo-check-verified (no toolchain on the authoring host) - self-reviewed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip both CI gates from informational to hard-fail (SPEC-001 quality gates):
- clippy: `-- -D warnings` on the server crate. Cleared the debt via clippy --fix
(unused imports/style), targeted #[allow(dead_code)] on native-remote-control
future API, and #[allow(clippy::too_many_arguments)] on 3 protocol-mirroring fns.
- cargo audit: hard-fail with documented per-ID --ignore flags (rsa RUSTSEC-2023-0071
unfixable/unreachable in active tree; gtk-rs + glib Linux-only tray backend not
compiled into the Windows agent; proc-macro-error build-time). New advisories fail.
- Move [profile.release] to the workspace root (it was silently ignored in the server
member), activating lto/codegen-units/strip.
No behavioral changes. Reviewed and gates verified passing on the build host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced
pre-existing formatting drift across the agent and server crates. Apply rustfmt
across the workspace so the codebase meets its own CI gate. Pure formatting; no
logic changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings azcomputerguru/guru-connect up to the authoritative working copy that
had been maintained in the claudetools monorepo: Phase 1 security and
infrastructure (middleware, metrics, utils, token blacklist, deployment
scripts, security audits) plus the native-remote-control integration spec.
Preserves the repo .gitignore, .cargo, and server/static/downloads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added organization, site, tags columns to connect_machines table
- Agent now sends org/site/tags from embedded config in AgentStatus
- Server stores org/site/tags metadata in database
- Enables grouping machines by client/site/tag in dashboard
Features:
- Agent checks for updates periodically (hourly) during idle
- Admin can trigger immediate updates via dashboard "Update Agent" button
- Silent updates with in-place binary replacement (no reboot required)
- SHA-256 checksum verification before installation
- Semantic version comparison
Server changes:
- New releases table for tracking available versions
- GET /api/version endpoint for agent polling (unauthenticated)
- POST /api/machines/:id/update endpoint for admin push updates
- Release management API (/api/releases CRUD)
- Track agent_version in machine status
Agent changes:
- New update.rs module with download/verify/install/restart logic
- Handle ADMIN_UPDATE WebSocket command for push updates
- --post-update flag for cleanup after successful update
- Periodic update check in idle loop (persistent agents only)
- agent_version included in AgentStatus messages
Dashboard changes:
- Version display in machine detail panel
- "Update Agent" button for each connected machine
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- REST API: All session/code/machine endpoints now require AuthenticatedUser
- Viewer WebSocket: Requires JWT token in query params (token=...)
- Agent WebSocket: Requires either valid support code OR API key
- Dashboard: Passes JWT token when connecting to viewer WS
- Native viewer: Passes token in protocol URL and WebSocket connection
- Added AGENT_API_KEY env var support for persistent agents
- Added get_status() to SupportCodeManager for auth validation
This fixes the security vulnerability where unauthenticated agents
could connect and appear in the dashboard without any credentials.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add AdminCommand message to protobuf (uninstall, restart, update)
- Add DELETE /api/machines/:agent_id endpoint with options:
- ?uninstall=true - send uninstall command to online agent
- ?export=true - return session history before deletion
- Add GET /api/machines/:agent_id/history endpoint for history export
- Add GET /api/machines endpoint to list all machines
- Handle AdminCommand in agent session handler
- Handle ADMIN_UNINSTALL error in agent main loop to trigger uninstall
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add connect_machines, connect_sessions, connect_session_events, connect_support_codes tables
- Implement db module with connection pooling (sqlx)
- Add machine persistence across server restarts
- Add audit logging for session/viewer events
- Support codes now persisted to database
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ViewerInfo struct to track viewer name and connection time
- Update session manager to track viewers with names
- Update API to return viewer list for each session
- Update dashboard to display "Mike Connected (3 min)" on machine bars
- Update viewer.html to pass viewer_name parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Sessions now track whether agent is online or offline
- Persistent agents (no support code) stay in session list when disconnected
- Dashboard shows online/offline status with color indicator
- Connect/Chat buttons disabled when agent is offline
- Agent reconnection reuses existing session
- Create viewer.html with canvas-based video display
- Implement protobuf parsing for VideoFrame/RawFrame
- Add zstd decompression using fzstd library
- Convert BGRA to RGBA for canvas rendering
- Add mouse event capture and encoding
- Add keyboard event capture and encoding
- Add Ctrl+Alt+Del special key support
- Add fullscreen toggle
- Update dashboard to open viewer in new window
- Auto-reconnect on connection loss
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add StartStream/StopStream/AgentStatus messages to protobuf
- Agent now starts in idle mode (heartbeat only, no capture)
- Agent enters streaming mode when viewer connects (StartStream)
- Agent returns to idle when all viewers disconnect (StopStream)
- Server tracks viewer IDs and sends start/stop commands
- Heartbeat mechanism with 90 second timeout detection
- Session API now includes streaming status and agent info
This allows 2000+ agents to connect with minimal bandwidth.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ChatMessage to protobuf definitions
- Server relays chat messages between agent and viewer
- Agent chat module shows messages via MessageBox
- Dashboard chat modal with WebSocket connection
- Simplified protobuf encoder/decoder in JavaScript
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Server changes:
- Allow cancelling connected codes (not just pending)
- Reject agent connections with cancelled codes
- Periodic cancellation check during active sessions
- Send Disconnect message when code is cancelled
Agent changes:
- Detect cancellation via Disconnect message
- Show Windows MessageBox to notify user
- Exit cleanly without reconnecting for support sessions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Support tab auto-refreshes every 3 seconds
- Shows client hostname under code when connected
- Changes Cancel button to Join button when connected
- Added joinSession placeholder function
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Server: Accept support_code param in WebSocket connection
- Server: Link code to session when agent connects, mark as connected
- Server: Mark code as completed when agent disconnects
- Agent: Accept support code from command line argument
- Agent: Send hostname and support_code in WebSocket params
- Portal: Trigger agent download with code in filename
- Portal: Show code reminder in download instructions
- Dashboard: Add machines list fetching (Access tab)
- Add TODO.md for feature tracking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /login page with dark theme matching portal
- Add /dashboard with 4 tabs: Support, Access, Build, Settings
- Add clean URL routes (/login, /dashboard) to server
- Add "Technician Login" link to portal footer
- Dashboard shows active support codes with generate/cancel
- Build tab has installer builder form (placeholder for agent)
- Access tab has 3-panel layout for machine management
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Portal:
- Code entry page with dark theme
- Browser detection for download instructions
- Custom protocol handler support
- Mobile-friendly numeric input
Server:
- Support codes module (6-digit generation, validation)
- Static file serving for portal
- New API endpoints: /api/codes/*
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>