The Phase A work passed cargo check + clippy + tests locally but missed
`cargo fmt --all -- --check` (the first step of the Linux CI job): module
ordering in db/mod.rs and two trailing-comment alignments in rate_limit.rs.
No logic change. Agent build failure on the prior run was transient infra
(verified: agent crate compiles clean locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies the four review fixes to POST /api/enroll, all in server/src/api/enroll.rs
(+ a new ENROLL_SITE_CONFLICT event type in server/src/db/events.rs):
1. HIGH — close the within-tenant cross-site silent-move hijack. A valid key for
site B presented for a machine_uid already bound to a DIFFERENT site is now
REFUSED (409 ENROLL_SITE_CONFLICT) instead of silently repointing the row and
minting a fresh cak_. No move, no key. Emits an ENROLL_SITE_CONFLICT audit event
+ alert TODO. Same-site match still resolves to reuse; a NULL prior site_id is a
first relational bind, not a move. The unauthenticated site_move mint path is
removed; deliberate moves are deferred to the Phase-B --reassign flow + dashboard.
2. MEDIUM — kill the timing/enumeration oracle. Unknown site_code and no-active-key
early rejects now pay a dummy Argon2id verify against a fixed, valid throwaway PHC
constant (TIMING_EQUALIZER_PHC) before returning the identical 401, so every
rejection path pays one KDF. The constant is asserted valid + verifying in tests.
3. LOW — fix the new-enroll TOCTOU. The dedup lookup + INSERT is wrapped in a bounded
retry loop: a concurrent first-enroll of the same machine_uid whose INSERT loses
the unique-index race (classified by is_machine_uid_conflict on SQLSTATE 23505 +
machine_uid constraint) now re-looks-up and converges to reuse instead of 500ing.
A non-machine_uid unique violation still surfaces as 500.
4. LOW — make the collision-gate doc honest + leave an enforcement TODO. The module
doc now states the gate withholds only a NEWLY minted cak_ (a prior clean cak_
survives) and that nothing consults enrollment_state at control time yet, with a
TODO(SPEC-016 Phase B/D) marker for relay/control-plane enforcement + revocation.
Verify: cargo check, cargo clippy --all-targets, and cargo test all clean on this
Windows host (104 tests pass). Two DB-gated tests (cross-site bound-site_id exposure,
machine_uid-vs-agent_id conflict classification) no-op without TEST_DATABASE_URL and
run against real Postgres in CI; the Linux target / real-Postgres handler path is
validated there, not on this host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server-side zero-touch per-site enrollment (Phase A: backend + DB only;
agent-side machine_uid derivation is Phase B, server treats it as opaque).
Migration 010_spec016_enrollment.sql:
- connect_sites: relational site anchor (site_code natural key, per-tenant
unique). The spec assumed a sites table existed; it did not (site/company
were free-text columns on connect_machines), so this creates a minimal one.
- site_enrollment_keys: rotatable, Argon2id-hashed cek_ secret + monotonic
version + hex fingerprint + active flag; one-active-per-site partial unique.
- connect_machines: + site_id (FK), + enrollment_state ('active'|'pending')
collision gate, + per-tenant (tenant_id, machine_uid) unique index added
ALONGSIDE the 008 global index (the connect-path upsert_machine ON CONFLICT
arbiter binds to 008 — dropping it would break live reconnect).
- connect_sites.enrollment_policy: reserved (default auto-approve), not enforced.
auth/enrollment_keys.rs: cek_ mint (256-bit, OS CSPRNG), Argon2id hash/verify
(reuses auth::password), and hex fingerprint vN (XXXX) per resolved-decision #3.
db/sites.rs + db/enrollment_keys.rs: runtime sqlx persistence; rotate_key
deactivates+inserts in one tx to hold the one-active-key invariant.
POST /api/enroll (public, api/enroll.rs): site_code+cek_ verify against active
key -> dedup on (tenant, machine_uid) -> new / reuse / site-move / collision.
Collision gate (PROVISIONAL heuristic: online existing row + different hostname)
-> pending, no usable cak_, alert. Mints cak_ via existing agent_keys path in the
exact form relay::validate_agent_api_key expects. Per-(site_code,IP) rate-limit +
lockout (EnrollLimiter). Audit events + [ENROLL] alert markers with
TODO(SPEC-016) #dev-alerts notes.
Admin (JWT) api/sites.rs: POST /api/sites/:id/enrollment-key/rotate (plaintext +
fingerprint once) and GET .../enrollment-key (fingerprint/version, no secret).
Routes wired in main.rs (enroll public, rotation admin). 13 new unit tests;
full server suite 99 passing. cargo check + clippy clean on the host (Windows)
target — Linux cross-target not installed here; server crate is platform-neutral
Rust. No sqlx offline cache needed (codebase uses runtime queries, no query!).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin-gated soft-delete + purge so operators can clear ghost machines/sessions
(the ~15-rows-for-one-host accumulation) from the console.
- migration 009: deleted_at on connect_sessions + connect_machines, with partial
indexes WHERE deleted_at IS NULL.
- DELETE /api/machines/:agent_id?purge=true and DELETE /api/sessions/:id?purge=true
soft-delete the row and purge the in-memory session (remove_session); the
non-purge path keeps the legacy hard-delete / live-only disconnect. POST
/api/machines/bulk-remove handles multi-select (batch cap 500). All admin-gated
(AdminUser -> 403; tightens the prior any-user delete) and audited to
connect_session_events (actor + target + trusted client IP).
- list/get queries filter deleted_at IS NULL so removed units leave the console;
upsert revives (deleted_at = NULL) a genuinely-reconnecting machine. The
keyed-reattach identity resolver (get_machine_by_id) is intentionally unfiltered.
Dashboard removal UI is the A3b follow-up. 86 server tests pass; fmt/clippy/test
clean. Implements specs/v2-stable-identity/plan.md Task 5 (server portion).
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>
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>
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>
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>
- 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 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
- 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>