Persist the agent-reported machine_uid and dedup connect_machines on it so a
single physical machine can't register duplicate rows when its config-file
agent_id regenerates (the ghost-session root cause).
- migration 008: nullable connect_machines.machine_uid + partial unique index
(WHERE machine_uid IS NOT NULL); idempotent, startup-applied.
- upsert_machine: two-path dedup (ON CONFLICT machine_uid when present, else
the legacy ON CONFLICT agent_id path, unchanged).
- session reattach: a machine_uid index consulted before agent_id, with all
removal paths purging it.
- security: keyed (cak_) agents stay authoritative — their claimed machine_uid
is dropped (effective_machine_uid=None); uid is dedup-only for un-keyed /
support-code agents. Startup restore skips uid-indexing keyed machines and
fails closed if the keyed-set query errors.
74 server tests pass; clippy clean. Implements specs/v2-stable-identity/plan.md Task 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native viewer's H.264 path (Task 7 first-cut, compile-verified only)
never rendered a frame. Three stacked bugs, all confirmed via live loopback:
1. decoder: MF_E_NOTACCEPTING (0xC00D36B5) was treated as fatal and only
one output was drained per call, so once the MFT filled it rejected
every subsequent frame. decode() now returns Vec<DecodedFrame>, drains
on back-pressure and retries the unconsumed sample, then drains all
ready outputs.
2. decoder: the NV12 output type was hand-built and rejected by the MS
H.264 decoder MFT (MF_E_TRANSFORM_TYPE_NOT_SET, 0xC00D6D60). It is now
negotiated by enumerating GetOutputAvailableType on STREAM_CHANGE /
TYPE_NOT_SET.
3. render: a manual pump_messages() in about_to_wait stole winit's own
thread messages and froze the event loop after one iteration, so frames
were never drained from the channel. Removed; winit's run_app pump
already services the WH_KEYBOARD_LL hook.
Validated on a 5070 loopback: 0 decode errors, frames decode/paint/present
(present count 0 -> 1740). Reviewed (APPROVE-WITH-NITS); diagnostics stripped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comprehensive specification for branding/whitelabel configuration.
- Dashboard admin settings page (logo, brand hue, product name, company name, favicon)
- OKLCH color system with CSS variables for dynamic theming
- Agent tray tooltip customization via registry key
- Singleton database table with public GET endpoint
- Priority: P2, Effort: Medium (4-6 weeks)
- Added to roadmap under Server/API (v2 Phase 2)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Agent now derives a recomputable, opaque machine_uid (Windows: SHA-256 of the OS
MachineGuid at HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid -> muid_<hex>;
non-Windows / registry-failure: persisted random UUID, warn-logged). Raw GUID
never exposed; OnceLock-cached. Reported ALONGSIDE agent_id (unchanged) on
AgentStatus (new additive proto field 12) and in the connect handshake query.
This is the stable identity that fixes config-loss duplicate registrations
(DESKTOP-I66IM5Q x9); server-side dedup keying that consumes it is SPEC-004
Task 2. Non-breaking, isolated. 5 unit tests; cargo fmt/clippy(-D warnings)/test
green on GURU-5070.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ordered, execution-ready plan for SPEC-004 (stable machine identity + session
reaping + operator removal). Works out the core integration: machine_uid =
deterministic MachineGuid-based hardware identity (recomputable, so config loss
can't duplicate); per-agent cak_ key stays the credential/trust boundary; they
compose so one cak_ key per machine_uid = one key per real machine (the
prerequisite the fleet key-migration #7 needs). Root cause grounded in code:
agent_id is a random UUID (config.rs:90), connect_machines dedups on ON CONFLICT
(agent_id), so config loss -> duplicate rows (DESKTOP-I66IM5Q x9 live). 5 ordered
tasks (agent uid -> server dedup -> reconcile/age-out -> reaping -> operator
removal). Unblocks #7 -> #5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets the HW-H.264 path be live-validated on tagged test agents without affecting
the live client fleet. Adds H264_TEST_TAG="h264-test" + a pure prefer_h264_for(tags)
helper (DEFAULT_PREFER_H264 || tags contains the tag, case-insensitive); StartStream
codec negotiation now computes prefer_h264 from the agent's reported tags instead of
the bare const, and logs the computed value. SAFETY: untagged sessions are byte-for-
byte unchanged (prefer_h264 == DEFAULT_PREFER_H264 == false -> raw); the supports_h264
guard still forces raw for a no-HW agent even when tagged. DEFAULT_PREFER_H264 stays
false (flipping the global default is a separate future step). 3 unit tests added.
cargo fmt/clippy(-D warnings)/test green on GURU-5070 (37 agent + 64 server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Major update to SPEC-012 adding dual-mode terminal access:
Mode 1: Serial Console Mode (True Remote Console)
- Direct access to system serial console (/dev/ttyS0 or /dev/console)
- Sees GRUB bootloader, kernel boot messages, login prompts, kernel panics
- Boot-time interaction: select GRUB entries, edit kernel parameters, single-user mode
- Requires root privileges or CAP_SYS_TTY_CONFIG capability
- Setup: GRUB + kernel parameters configured for serial console output
- Like KVM-over-IP or IPMI Serial-over-LAN (text-mode equivalent)
Mode 2: PTY Shell Mode (Interactive Shell)
- Spawn pseudo-TTY with bash/zsh shell session
- Normal server management (package updates, log review, etc.)
- Runs as unprivileged agent service user
- Standard interactive shell with full ANSI/VT100 support
Architecture:
- Agent mode selection based on viewer request (console vs. shell)
- Dashboard shows two buttons: "Console" and "Shell" for headless agents
- Same xterm.js viewer handles both modes transparently
- Protobuf extensions: TerminalModeRequest enum, console_mode flag
Security:
- Console mode requires root (boot-level control risk)
- Recommend RBAC: separate console_access and shell_access permissions
- Console sessions should require MFA (Phase 2)
- Audit logging for both modes
Setup Requirements:
- One-time GRUB configuration for serial console
- systemd service with CAP_SYS_TTY_CONFIG for console mode
- serial-getty@ttyS0.service enabled for login prompt
Updated effort: Medium (5-7 weeks, up from 4-6)
Priority remains P2
Addresses user request for "remote console" (as if at the machine)
not just shell access.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The auto-update path built both reqwest clients with an unconditional
danger_accept_invalid_certs(true), so a network MITM could serve an arbitrary
update .exe (checksum is no defense — same unverified channel) and gain RCE on
every managed endpoint. Replace with dev_insecure_tls() = cfg!(debug_assertions)
&& env GURUCONNECT_DEV_INSECURE_TLS: the cfg gate compiles out of release builds,
so a shipped agent ALWAYS verifies certs; dev keeps a self-signed escape hatch.
Loud warn when the insecure path is taken; verify_checksum kept + documented as
transport-integrity (not tamper) defense; TODO + follow-up for embedded-key
update signing (defense-in-depth). Release-invariant unit test added.
cargo fmt/clippy(-D warnings)/test green on GURU-5070 (90 tests). Closes the
2026-05-30 security-audit HIGH (reports/2026-05-30-gc-audit.md).
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>
Re-baseline against actual git/deploy state: secure-session-core Tasks 1-7 are
committed and DEPLOYED; the 3 audit CRITICALs are closed and live in prod
(verified: deployed checkout abc55ab descends from the CRITICAL#1 fix + Task 7;
guruconnect.service running on :3002). The prior "Sprint 0: bypasses are live"
banner was wrong (stale 2026-05-29 audit narrative) and is removed. Remaining
to exit Phase 1 = secure-session-core Task 8 (e2e verification + security
re-audit) + Code-Review sign-off on Tasks 3-5. Schema note corrected
(connect_agent_keys + tenancy already exist via migration 004).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark SPEC-003..009 as work-items inside the SPEC-002 v2 phases (not standalone
v1 backlog): banner records the v2-reset decision + the Sprint-0 relay-auth
CRITICAL hotfix, a phase-mapping table (004->P1, 008->P0/1, 003/005/006/007->P2,
009->P3), inline [-> v2 Phase N] tags per spec, and a note to bake SPEC-003
inventory cols + SPEC-004 machine_uid + connect_agent_keys into the Phase-0
fresh schema. Sprint planning 2026-05-30 (Mike: v2 reset first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Everything the console does should be callable by API, documented and
discoverable. Adds: OpenAPI 3.x generated from code (utoipa) + Swagger/Redoc at
/api/docs (drift-proof, route<->spec parity test); long-lived revocable scoped
API tokens (connect_api_tokens, hashed like agent keys) distinct from the 24h
dashboard JWT and agent keys; an API-completeness gap audit (folds in SPEC-004/
006/007 endpoints); consistent pagination/filtering + versioning policy. Today
there is zero API doc tooling and no programmatic token. Depends on SPEC-008 for
the documented error envelope; distinct from the ADR-001 integration contract.
Large. Parallel guru-rmm SPEC-019. Requested by Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-cutting error-quality initiative: one structured AppError envelope
(stable error_code + message + correlation_id) replacing the current ad-hoc
mix (bare (StatusCode,&str) tuples, per-file ErrorResponse, two JSON envelopes
the dashboard already unions); correlation-id middleware tied to tracing spans
+ response header so a reported id greps the log; contextual error logging with
identifiers + error chain; sweep the 37 server `let _ =` swallows (the pattern
that silently hid migration-005's missing columns); dashboard renders the real
cause + correlation id (drop the hardcoded generic at MachinesPage.tsx:202);
agent logs why/where auth/connection failed (the auth-loop incident gave no
local signal). Phaseable; Large. Parallel RMM request keeps conventions aligned.
Requested by Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dashboard "Build Installer" wizard for pre-labeled managed/persistent agents
(Name/Company/Site/Department/Device Type/Tag/Type) with Download / Copy URL /
Send Link, ScreenConnect-style. The embed-config build path already exists
(downloads.rs appends EmbeddedConfig GURUCONFIG blob; AgentDownloadParams takes
company/site/tags/api_key; agent reads it at config.rs:223) - missing is the UI,
department + device_type fields (EmbeddedConfig/AgentStatus/connect_machines),
name strategy, and Copy-URL/Send-Link actions. Labels persist at install time,
feeding SPEC-003/005/006. Embedded key should be revocable per-machine/site
(pairs with SPEC-004). Biggest open question: appending config after Authenticode
signing invalidates the signature. Requested by Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single search box matching case-insensitive substring across ALL machine
attributes (OS, logged-on user, external/private IP, company, site, tag,
serial, MAC, client version, ...) server-side, ScreenConnect-style. Replaces
the dashboard's hostname/agent_id-only client filter (inadequate at ~900+
machines). pg_trgm GIN index over a concatenated searchable-text expression
(INET cast to text, tags via array_to_string); multi-term AND; optional
field-scoped syntax (os:/user:/ip:). Parameterized + fixed column allowlist
(no injection), admin-guarded, DoS-capped. Depends on SPEC-003 (attrs must be
persisted to be searchable); reuses SPEC-005 enriched payload. Requested by
Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ScreenConnect "Access"-list parity for the Operator Console machines list:
per-row dual Host/Guest connection indicators (Guest=agent is_online,
Host=viewer_count>0 with viewer names + durations) and rich inline metadata
(company, site, device type, tags, logged-on user + idle, client version in
red when outdated). Live Host/Guest state already exists on SessionInfo
(is_online, viewer_count, viewers); main work is enriching /api/machines with
that + SPEC-003 inventory and redesigning MachinesPage rows. Depends on
SPEC-003 (data), reads cleanest after SPEC-004 (dedup), dovetails SPEC-002
Phase 2. Company-tree nav split out as a P3 follow-up. Requested by Mike
2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address duplicate registration at the source, not just via cleanup. Root
cause now grounded: agent_id is a random UUID (config.rs:90 generate_agent_id)
persisted only in the config file, so a portable/misconfigured execution
(the Pavon desktop launcher) regenerates a fresh id each launch, defeating
both the DB upsert (ON CONFLICT agent_id) and session-reuse dedupe. Add a
deterministic machine_uid (Windows MachineGuid-based, recomputable) keyed by
registration; reaping/supersede become defense-in-depth. Security: machine_uid
is identity not authorization and must be bound to the per-machine agent key
to prevent session/record hijack. Requested by Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop orphaned managed sessions accumulating in the Operator Console and let
admins remove stale sessions/units individually and in bulk. Root cause
confirmed in code: the Sessions list is the in-memory SessionManager;
register_agent reconnect-reuse keys on a stable agent_id (session/mod.rs:169)
and persistent sessions are never reaped on disconnect (session/mod.rs:519-542),
so an agent reconnecting with a fresh agent_id leaves a new retained ghost
session each time (observed: 15 sessions/0 live, ~10 orphans for one machine
after a GuruConnect-client reconnect storm). Adds TTL sweep + same-machine
supersede, admin-gated audited purge + bulk endpoints, and dashboard
multi-select removal. Requested by Mike 2026-05-30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist a complete per-machine device inventory on connect_machines
(OS+locale+install, CPU/RAM, mfr/model/serial, external WAN IP captured
server-side via trusted-proxy client_ip + private LAN IP + MAC, logged-on
user, idle, time zone, uptime, local-admin-present), refreshed each
AgentStatus and surfaced in the dashboard machine detail — ScreenConnect
"Guest Info" parity. Data layer for SPEC-002 Phase 2; closes the GC side
of the agent-IP gap (coord todo 7459428e). Requested by Mike 2026-05-30.
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>
Admin-only user management: list, create, edit role/permissions/status,
reset password, and disable/delete, against the v2 users API.
- Admin-gated three ways: AdminRoute on /users (calm access-denied panel
for non-admins, no redirect loop or data fetch), Sidebar hides the nav
item, and every mutation relies on the server AdminUser 403 as the real
authority. isAdmin is derived from the server-validated user, not the
client token.
- Users table: role badge (admin/operator/viewer), permissions summary,
enabled/disabled status, created, last-login. Sticky header, skeleton,
empty/error states. Self row tagged "You".
- Create/edit use the real roles and permission strings
(view/control/transfer/manage_users/manage_clients); admin permissions
are server-implicit and shown locked. Passwords: typed or Web Crypto
generated (rejection-sampled, copy-once reveal), type=password +
autoComplete=new-password, cleared from state on open/close/success,
never logged/persisted/in-URL; blank on edit means unchanged.
- Self-lockout guards: cannot disable, delete, or demote your own admin
account (controls disabled + submit-handler checks, matched on the
authoritative user id). Server mirrors self-disable/self-delete; the
self-demotion guard is client-side (server todo filed).
- useUpdateUser sequences user-update then permissions-set; invalidates
["users"] on settled so the table reconciles after a partial failure,
with an actionable message if only permissions failed.
Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green). Completes the v2 dashboard view set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generate, list, and cancel attended-support codes (XXX-XXX-XXX), built
on the v2 codes API and existing UI primitives.
- Codes table: code in mono, status badge (pending+pulse/connected/
completed/cancelled), bound client/machine, created-by, created
(relative + absolute tooltip). Sticky header, skeleton load,
actionable empty/error states.
- Generate opens a focused reveal modal showing the code large in
JetBrains Mono with copy and a read-aloud instruction; the code is
announced character-by-character for screen readers. Mint is ref-
guarded so it creates exactly one code per open (no StrictMode dupe).
- Cancel via confirm dialog (POST /api/codes/:code/cancel), disabled for
non-cancellable statuses; invalidates the codes query. List polls 7s.
- Shared API client now tolerates non-JSON 200 bodies, so the cancel
endpoint's plain-text "Code cancelled" success no longer surfaces as a
failure. Error-envelope handling unchanged.
Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green).
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>
Active-sessions table with consent-state badges, viewer-token Join,
and disconnect, built on the v2 session API and existing UI primitives.
- Sessions table: machine, mode (managed/attended), consent badge
(granted/pending+pulse/denied/not_required), viewers, started,
duration, status. Sticky header, skeleton load, empty/error states.
- Join action mints a session-scoped viewer token
(POST /api/sessions/:id/viewer-token) and reveals it with the
/ws/viewer relay URL and copy buttons. The static viewer.html is
intentionally not targeted: it sends the raw login JWT, which the v2
viewer plane rejects. In-dashboard web viewer ships in a later pass.
- Authz split mirrors the server mint gate: admin or control permission
gets Control; view permission gets View only; neither hides the action.
Server remains authoritative; the minted token carries the signed
access claim.
- Disconnect via confirm dialog (DELETE /api/sessions/:id), invalidates
the sessions query. List polls every 8s so consent transitions surface.
Passed Code Review (no blockers) and local gates (tsc/lint/build 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>
SPEC-002 Phase 1 Task 6, code-reviewed APPROVED (2 rounds), locally verified
(cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 70 pass + build).
- Viewer WH_KEYBOARD_LL hook diverts system combos (Win/Win+R, Alt+Tab, Alt+Esc,
Ctrl+Esc) to the remote as a full KeyEvent (vk + scan + is_extended + modifiers)
and suppresses local handling - GATED on the viewer window having focus AND a
"send system keys" toggle (default on; Pause/Break host-key), so it never bricks
the technician's local keyboard when unfocused.
- Agent injection via SendInput KEYEVENTF_SCANCODE + correct KEYEVENTF_EXTENDEDKEY
(right Ctrl/Alt, arrows, nav, Win, NumLock, numpad Divide) - layout-independent,
extended-key-correct.
- Ctrl+Alt+Del completes through the SAS helper (SYSTEM SendSAS); installer sets
the SoftwareSASGeneration policy; 3-tier fail-loud (no false success). SAS named
pipe DACL tightened from NULL/Everyone to Authenticated Users.
- Modifier hygiene: viewer emits key-ups for held Ctrl/Alt/Shift/Win on focus loss
/ close so modifiers never stick on the remote.
- proto: KeyEvent.is_extended = 7 (additive; older agents derive the flag).
Closes Win+R / Ctrl+C-V / Ctrl+Alt+Del / arrows-vs-numpad fidelity. Live on-device
testing is plan Task 8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI never ran clippy on the agent crate (the build-server clippy job is
Linux-only and can't compile the Windows agent; build-agent only runs cargo
build), so 77 clippy -D-warnings errors had accumulated. Behavior-preserving
cleanup, code-reviewed APPROVED, locally verified (cargo clippy --workspace
--all-targets --all-features -- -D warnings exits 0; cargo test --workspace =
57 passed).
- let _ = on Win32 resource-teardown BOOL returns (gdi.rs); fallible
BitBlt/GetDIBits stay error-handled
- removed unused imports/vars; idiom fixes (div_ceil, is_null, transmute
annotations, match collapsing, useless_conversion)
- #[allow(dead_code)] + comment on genuine Task-6/7 scaffolding (vk consts,
SpecialKey emission, SAS mgmt API, modifier tracking, GDI frame-diff fields)
- Cargo.lock: cargo pruned ~147 stale transitive entries (no version changes)
Follow-up: add cargo clippy -D warnings to the build-agent CI job so the agent
crate stays clippy-clean.
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>