diff --git a/server/cmd/api/api/ambient_mouse.go b/server/cmd/api/api/ambient_mouse.go new file mode 100644 index 00000000..d5e8618a --- /dev/null +++ b/server/cmd/api/api/ambient_mouse.go @@ -0,0 +1,262 @@ +package api + +import ( + "context" + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/onkernel/kernel-images/server/lib/logger" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +// ambientAction identifies the type of ambient event to emit. +type ambientAction int + +const ( + ambientMouseDrift ambientAction = iota + ambientScroll + ambientMicroDrag + ambientClick + ambientKeyTap +) + +// ambientConfig holds the resolved configuration for the ambient mouse loop. +type ambientConfig struct { + minIntervalMs int + maxIntervalMs int + weights []struct { + action ambientAction + weight int + } + totalWeight int +} + +func (s *ApiService) SetAmbientMouse(ctx context.Context, request oapi.SetAmbientMouseRequestObject) (oapi.SetAmbientMouseResponseObject, error) { + log := logger.FromContext(ctx) + + if request.Body == nil { + return oapi.SetAmbientMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + s.inputMu.Lock() + + // Stop any running ambient loop first. + if s.ambientCancel != nil { + s.ambientCancel() + s.ambientCancel = nil + } + + if !body.Enabled { + s.inputMu.Unlock() + log.Info("ambient mouse disabled") + return oapi.SetAmbientMouse200JSONResponse(oapi.AmbientMouseResponse{Enabled: false}), nil + } + + // Resolve configuration with defaults. + cfg, err := resolveAmbientConfig(body) + if err != nil { + s.inputMu.Unlock() + return oapi.SetAmbientMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: err.Error()}, + }, nil + } + + ambientCtx, cancel := context.WithCancel(context.Background()) + s.ambientCancel = cancel + s.inputMu.Unlock() + + go s.runAmbientLoop(ambientCtx, cfg) + + log.Info("ambient mouse enabled", + "min_interval_ms", cfg.minIntervalMs, + "max_interval_ms", cfg.maxIntervalMs, + ) + return oapi.SetAmbientMouse200JSONResponse(oapi.AmbientMouseResponse{Enabled: true}), nil +} + +// resolveAmbientConfig builds an ambientConfig from the request body, applying defaults. +func resolveAmbientConfig(body oapi.AmbientMouseRequest) (ambientConfig, error) { + cfg := ambientConfig{ + minIntervalMs: 200, + maxIntervalMs: 600, + } + if body.MinIntervalMs != nil { + cfg.minIntervalMs = *body.MinIntervalMs + } + if body.MaxIntervalMs != nil { + cfg.maxIntervalMs = *body.MaxIntervalMs + } + if cfg.minIntervalMs > cfg.maxIntervalMs { + return cfg, fmt.Errorf("min_interval_ms must be <= max_interval_ms") + } + + driftW := 55 + scrollW := 20 + microDragW := 12 + clickW := 10 + keyTapW := 3 + if body.MouseDriftWeight != nil { + driftW = *body.MouseDriftWeight + } + if body.ScrollWeight != nil { + scrollW = *body.ScrollWeight + } + if body.MicroDragWeight != nil { + microDragW = *body.MicroDragWeight + } + if body.ClickWeight != nil { + clickW = *body.ClickWeight + } + if body.KeyTapWeight != nil { + keyTapW = *body.KeyTapWeight + } + + cfg.weights = []struct { + action ambientAction + weight int + }{ + {ambientMouseDrift, driftW}, + {ambientScroll, scrollW}, + {ambientMicroDrag, microDragW}, + {ambientClick, clickW}, + {ambientKeyTap, keyTapW}, + } + for _, w := range cfg.weights { + cfg.totalWeight += w.weight + } + if cfg.totalWeight == 0 { + return cfg, fmt.Errorf("at least one action weight must be > 0") + } + return cfg, nil +} + +// runAmbientLoop is the background goroutine that emits diverse input events. +// It acquires inputMu for each action, so it cooperates with explicit API calls. +func (s *ApiService) runAmbientLoop(ctx context.Context, cfg ambientConfig) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + for { + select { + case <-ctx.Done(): + return + default: + } + + action := pickAmbientAction(r, cfg) + s.inputMu.Lock() + s.execAmbientAction(ctx, r, action) + s.inputMu.Unlock() + + // Random delay between events. + delayMs := cfg.minIntervalMs + r.Intn(cfg.maxIntervalMs-cfg.minIntervalMs+1) + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(delayMs) * time.Millisecond): + } + } +} + +func pickAmbientAction(r *rand.Rand, cfg ambientConfig) ambientAction { + n := r.Intn(cfg.totalWeight) + for _, w := range cfg.weights { + if n < w.weight { + return w.action + } + n -= w.weight + } + return ambientMouseDrift +} + +// execAmbientAction performs a single ambient event via xdotool. Must be called +// with inputMu held. +func (s *ApiService) execAmbientAction(ctx context.Context, r *rand.Rand, action ambientAction) { + switch action { + case ambientMouseDrift: + dx := r.Intn(8) - 4 + dy := r.Intn(8) - 4 + if dx == 0 && dy == 0 { + dx = 1 + } + defaultXdoTool.Run(ctx, "mousemove_relative", "--", fmt.Sprintf("%d", dx), fmt.Sprintf("%d", dy)) + + case ambientScroll: + w, h := s.getDisplayGeometry(ctx) + if w > 0 && h > 0 { + x := w/2 + r.Intn(80) - 40 + y := h/2 + r.Intn(80) - 40 + defaultXdoTool.Run(ctx, "mousemove", strconv.Itoa(x), strconv.Itoa(y)) + btn := "4" + if r.Intn(2) == 0 { + btn = "5" + } + defaultXdoTool.Run(ctx, "click", btn) + } + + case ambientMicroDrag: + dx := 3 + r.Intn(6) + dy := 3 + r.Intn(6) + if r.Intn(2) == 0 { + dx = -dx + } + if r.Intn(2) == 0 { + dy = -dy + } + defaultXdoTool.Run(ctx, "mousedown", "1") + defaultXdoTool.Run(ctx, "mousemove_relative", "--", fmt.Sprintf("%d", dx), fmt.Sprintf("%d", dy)) + // Use background context so mouseup always fires even if ctx is cancelled, + // preventing a stuck mouse button. + defaultXdoTool.Run(context.Background(), "mouseup", "1") + + case ambientClick: + w, h := s.getDisplayGeometry(ctx) + if w > 200 && h > 200 { + pad := 100 + if w < 400 { + pad = w / 4 + } + x := pad + r.Intn(max(1, w-2*pad)) + y := pad + r.Intn(max(1, h-2*pad)) + defaultXdoTool.Run(ctx, "mousemove", strconv.Itoa(x), strconv.Itoa(y)) + defaultXdoTool.Run(ctx, "click", "1") + } + + case ambientKeyTap: + // Modifier tap; least likely to trigger page behavior. + defaultXdoTool.Run(ctx, "key", "shift") + } +} + +// getDisplayGeometry returns the current display dimensions via xdotool. +// The result is cached for 30 seconds to avoid shelling out on every ambient event. +func (s *ApiService) getDisplayGeometry(ctx context.Context) (int, int) { + s.displayGeomMu.Lock() + defer s.displayGeomMu.Unlock() + + if time.Since(s.displayGeomAt) < 30*time.Second && s.displayGeomW > 0 { + return s.displayGeomW, s.displayGeomH + } + + out, err := defaultXdoTool.Run(ctx, "getdisplaygeometry") + if err != nil { + return s.displayGeomW, s.displayGeomH + } + parts := strings.Fields(strings.TrimSpace(string(out))) + if len(parts) >= 2 { + w, _ := strconv.Atoi(parts[0]) + h, _ := strconv.Atoi(parts[1]) + if w > 0 && h > 0 { + s.displayGeomW = w + s.displayGeomH = h + s.displayGeomAt = time.Now() + return w, h + } + } + return s.displayGeomW, s.displayGeomH +} diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 910410b9..6782888c 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -53,6 +53,15 @@ type ApiService struct { // policy management policy *policy.Policy + + // ambientCancel stops the ambient mouse loop (protected by inputMu) + ambientCancel context.CancelFunc + + // displayGeom caches xdotool getdisplaygeometry to avoid shelling out per ambient event + displayGeomMu sync.Mutex + displayGeomW int + displayGeomH int + displayGeomAt time.Time } var _ oapi.StrictServerInterface = (*ApiService)(nil) @@ -298,5 +307,13 @@ func (s *ApiService) ListRecorders(ctx context.Context, _ oapi.ListRecordersRequ } func (s *ApiService) Shutdown(ctx context.Context) error { + // Stop ambient mouse loop if running. + s.inputMu.Lock() + if s.ambientCancel != nil { + s.ambientCancel() + s.ambientCancel = nil + } + s.inputMu.Unlock() + return s.recordManager.StopAll(ctx) } diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index be7766ff..7d62eb47 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "log/slog" "math" + "math/rand" "os" "os/exec" "strconv" @@ -15,6 +17,7 @@ import ( "time" "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/mousetrajectory" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -51,34 +54,32 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest return &validationError{msg: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)} } - // Build xdotool arguments - args := []string{} + useSmooth := body.Smooth == nil || *body.Smooth // default true when omitted + if useSmooth { + return s.doMoveMouseSmooth(ctx, log, body) + } + return s.doMoveMouseInstant(ctx, log, body) +} - // Hold modifier keys (keydown) +func (s *ApiService) doMoveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { + args := []string{} if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keydown", key) } } - - // Move the cursor to the desired coordinates args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) - - // Release modifier keys (keyup) if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keyup", key) } } - log.Info("executing xdotool", "args", args) - output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) return &executionError{msg: "failed to move mouse"} } - return nil } @@ -100,6 +101,111 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques return oapi.MoveMouse200Response{}, nil } +func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { + fromX, fromY, err := s.getMouseLocation(ctx) + if err != nil { + log.Error("failed to get mouse location for smooth move", "error", err) + return &executionError{msg: "failed to get current mouse position: " + err.Error()} + } + + // When duration_sec is specified, compute the number of trajectory points + // to achieve that duration at a ~10ms step delay (human-like event frequency). + // Otherwise let the library auto-compute from path length. + const defaultStepDelayMs = 10 + var opts *mousetrajectory.Options + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 { + durationMs := int(*body.DurationSec * 1000) + targetPoints := durationMs / defaultStepDelayMs + if targetPoints < mousetrajectory.MinPoints { + targetPoints = mousetrajectory.MinPoints + } + if targetPoints > mousetrajectory.MaxPoints { + targetPoints = mousetrajectory.MaxPoints + } + opts = &mousetrajectory.Options{MaxPoints: targetPoints} + } + + traj := mousetrajectory.NewHumanizeMouseTrajectoryWithOptions( + float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) + points := traj.GetPointsInt() + if len(points) < 2 { + return s.doMoveMouseInstant(ctx, log, body) + } + + // Compute per-step delay to achieve the target duration. + numSteps := len(points) - 1 + stepDelayMs := defaultStepDelayMs + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 && numSteps > 0 { + durationMs := int(*body.DurationSec * 1000) + stepDelayMs = durationMs / numSteps + if stepDelayMs < 3 { + stepDelayMs = 3 + } + } + + // Hold modifiers + if body.HoldKeys != nil { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keydown", key) + } + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool keydown failed", "err", err, "output", string(output)) + return &executionError{msg: "failed to hold modifier keys"} + } + defer func() { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) + } + // Use background context for cleanup so keys are released even on cancellation. + _, _ = defaultXdoTool.Run(context.Background(), args...) + }() + } + + // Move along Bezier path: mousemove_relative for each step with delay + for i := 1; i < len(points); i++ { + select { + case <-ctx.Done(): + return &executionError{msg: "mouse movement cancelled"} + default: + } + + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + if dx == 0 && dy == 0 { + continue + } + args := []string{"mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy)} + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i) + return &executionError{msg: "failed during smooth mouse movement"} + } + jitter := stepDelayMs + if stepDelayMs > 3 { + jitter = stepDelayMs + rand.Intn(5) - 2 + if jitter < 3 { + jitter = 3 + } + } + if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil { + return &executionError{msg: "mouse movement cancelled"} + } + } + + log.Info("executed smooth mouse movement", "points", len(points)) + return nil +} + +// getMouseLocation returns the current cursor position via xdotool getmouselocation --shell. +func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) { + output, err := defaultXdoTool.Run(ctx, "getmouselocation", "--shell") + if err != nil { + return 0, 0, fmt.Errorf("xdotool getmouselocation failed: %w (output: %s)", err, string(output)) + } + return parseMousePosition(string(output)) +} + func (s *ApiService) doClickMouse(ctx context.Context, body oapi.ClickMouseRequest) error { log := logger.FromContext(ctx) diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go new file mode 100644 index 00000000..5bdc81b0 --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -0,0 +1,239 @@ +package mousetrajectory + +import ( + "math" + "math/rand" +) + +// HumanizeMouseTrajectory generates human-like mouse movement points from (fromX, fromY) +// to (toX, toY) using Bezier curves with randomized control points, distortion, and easing. +// +// Ported from Camoufox MouseTrajectories.hpp, which was adapted from: +// https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py +type HumanizeMouseTrajectory struct { + fromX, fromY float64 + toX, toY float64 + points [][2]float64 + rng *rand.Rand +} + +// Options configures trajectory generation. +type Options struct { + // MaxPoints overrides the auto-computed point count. 0 = auto. Range 5-80. + MaxPoints int +} + +// NewHumanizeMouseTrajectory creates a trajectory from (fromX, fromY) to (toX, toY). +// Uses the default entropy source for randomization. +func NewHumanizeMouseTrajectory(fromX, fromY, toX, toY float64) *HumanizeMouseTrajectory { + return NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY, nil) +} + +// NewHumanizeMouseTrajectoryWithOptions creates a trajectory with optional overrides. +func NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY float64, opts *Options) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(rand.Int63())), + } + t.generateCurve(opts) + return t +} + +// NewHumanizeMouseTrajectoryWithSeed creates a trajectory with a fixed seed (for tests). +func NewHumanizeMouseTrajectoryWithSeed(fromX, fromY, toX, toY float64, seed int64) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(seed)), + } + t.generateCurve(nil) + return t +} + +// GetPoints returns the trajectory as a slice of [x, y] pairs (floats, caller rounds). +func (t *HumanizeMouseTrajectory) GetPoints() [][2]float64 { + return t.points +} + +// GetPointsInt returns the trajectory as integer coordinates suitable for xdotool. +func (t *HumanizeMouseTrajectory) GetPointsInt() [][2]int { + out := make([][2]int, len(t.points)) + for i, p := range t.points { + out[i][0] = int(math.Round(p[0])) + out[i][1] = int(math.Round(p[1])) + } + return out +} + +const ( + // Bounds padding for Bezier control point region (pixels beyond start/end). + boundsPadding = 80 + // Number of internal knots for the Bezier curve (more = curvier). + knotsCount = 2 + // Distortion parameters for human-like jitter: mean, stdev, frequency. + distortionMean = 1.0 + distortionStDev = 1.0 + distortionFreq = 0.5 +) + +const ( + defaultMaxPoints = 150 // Upper bound for auto-computed point count + defaultMinPoints = 0 // Lower bound for auto-computed point count (before clamp to MinPoints) + pathLengthScale = 20 // Multiplier for path-length-based point count + // MinPoints is the minimum number of trajectory points. + MinPoints = 5 + // MaxPoints is the maximum number of trajectory points. + MaxPoints = 80 +) + +func (t *HumanizeMouseTrajectory) generateCurve(opts *Options) { + left := math.Min(t.fromX, t.toX) - boundsPadding + right := math.Max(t.fromX, t.toX) + boundsPadding + down := math.Min(t.fromY, t.toY) - boundsPadding + up := math.Max(t.fromY, t.toY) + boundsPadding + + knots := t.generateInternalKnots(left, right, down, up, knotsCount) + curvePoints := t.generatePoints(knots) + curvePoints = t.distortPoints(curvePoints, distortionMean, distortionStDev, distortionFreq) + t.points = t.tweenPoints(curvePoints, opts) +} + +func (t *HumanizeMouseTrajectory) generateInternalKnots(l, r, d, u float64, knotsCount int) [][2]float64 { + knotsX := t.randomChoiceDoubles(l, r, knotsCount) + knotsY := t.randomChoiceDoubles(d, u, knotsCount) + knots := make([][2]float64, knotsCount) + for i := 0; i < knotsCount; i++ { + knots[i] = [2]float64{knotsX[i], knotsY[i]} + } + return knots +} + +func (t *HumanizeMouseTrajectory) randomChoiceDoubles(min, max float64, size int) []float64 { + out := make([]float64, size) + for i := 0; i < size; i++ { + out[i] = min + t.rng.Float64()*(max-min) + } + return out +} + +func factorial(n int) int64 { + if n < 0 { + return -1 + } + result := int64(1) + for i := 2; i <= n; i++ { + result *= int64(i) + } + return result +} + +func binomial(n, k int) float64 { + return float64(factorial(n)) / (float64(factorial(k)) * float64(factorial(n-k))) +} + +func bernsteinPolynomialPoint(x float64, i, n int) float64 { + return binomial(n, i) * math.Pow(x, float64(i)) * math.Pow(1-x, float64(n-i)) +} + +func bernsteinPolynomial(points [][2]float64, t float64) [2]float64 { + n := len(points) - 1 + var x, y float64 + for i := 0; i <= n; i++ { + bern := bernsteinPolynomialPoint(t, i, n) + x += points[i][0] * bern + y += points[i][1] * bern + } + return [2]float64{x, y} +} + +func (t *HumanizeMouseTrajectory) generatePoints(knots [][2]float64) [][2]float64 { + midPtsCnt := int(math.Max(math.Max(math.Abs(t.fromX-t.toX), math.Abs(t.fromY-t.toY)), 2)) + controlPoints := make([][2]float64, 0, len(knots)+2) + controlPoints = append(controlPoints, [2]float64{t.fromX, t.fromY}) + controlPoints = append(controlPoints, knots...) + controlPoints = append(controlPoints, [2]float64{t.toX, t.toY}) + + curvePoints := make([][2]float64, midPtsCnt) + for i := 0; i < midPtsCnt; i++ { + tt := float64(i) / float64(midPtsCnt-1) + curvePoints[i] = bernsteinPolynomial(controlPoints, tt) + } + return curvePoints +} + +func (t *HumanizeMouseTrajectory) distortPoints(points [][2]float64, distortionMean, distortionStDev, distortionFreq float64) [][2]float64 { + if len(points) < 3 { + return points + } + distorted := make([][2]float64, len(points)) + distorted[0] = points[0] + + for i := 1; i < len(points)-1; i++ { + x, y := points[i][0], points[i][1] + if t.rng.Float64() < distortionFreq { + delta := math.Round(normalDist(t.rng, distortionMean, distortionStDev)) + y += delta + } + distorted[i] = [2]float64{x, y} + } + distorted[len(points)-1] = points[len(points)-1] + return distorted +} + +func normalDist(rng *rand.Rand, mean, stdDev float64) float64 { + // Box-Muller transform + u1 := rng.Float64() + u2 := rng.Float64() + if u1 <= 0 { + u1 = 1e-10 + } + return mean + stdDev*math.Sqrt(-2*math.Log(u1))*math.Cos(2*math.Pi*u2) +} + +func (t *HumanizeMouseTrajectory) easeOutQuad(n float64) float64 { + return -n * (n - 2) +} + +func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options) [][2]float64 { + var totalLength float64 + for i := 1; i < len(points); i++ { + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + totalLength += math.Sqrt(dx*dx + dy*dy) + } + + targetPoints := int(math.Min( + float64(defaultMaxPoints), + math.Max(float64(defaultMinPoints+2), math.Pow(totalLength, 0.25)*pathLengthScale))) + + if opts != nil && opts.MaxPoints > 0 { + maxPts := opts.MaxPoints + if maxPts < MinPoints { + maxPts = MinPoints + } + if maxPts > MaxPoints { + maxPts = MaxPoints + } + targetPoints = maxPts + } + + if targetPoints < 2 { + targetPoints = 2 + } + + res := make([][2]float64, targetPoints) + for i := 0; i < targetPoints; i++ { + tt := float64(i) / float64(targetPoints-1) + easedT := t.easeOutQuad(tt) + idx := int(easedT * float64(len(points)-1)) + if idx < 0 { + idx = 0 + } + if idx >= len(points) { + idx = len(points) - 1 + } + res[i] = points[idx] + } + return res +} diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go new file mode 100644 index 00000000..e2915b48 --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -0,0 +1,87 @@ +package mousetrajectory + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHumanizeMouseTrajectory_DeterministicWithSeed(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points1 := traj.GetPointsInt() + + traj2 := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points2 := traj2.GetPointsInt() + + require.Len(t, points1, len(points2)) + for i := range points1 { + assert.Equal(t, points1[i], points2[i], "point %d should match", i) + } +} + +func TestHumanizeMouseTrajectory_StartAndEnd(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(50, 50, 200, 150, 123) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "should have at least 2 points") + assert.Equal(t, 50, points[0][0]) + assert.Equal(t, 50, points[0][1]) + assert.Equal(t, 200, points[len(points)-1][0]) + assert.Equal(t, 150, points[len(points)-1][1]) +} + +func TestHumanizeMouseTrajectory_WithStepsOverride(t *testing.T) { + opts := &Options{MaxPoints: 15} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, 15, "should have exactly 15 points when MaxPoints=15") +} + +func TestHumanizeMouseTrajectory_ZeroLengthPath(t *testing.T) { + // Same start and end: should produce at least 2 points, both at (0,0) + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 0, 0, 42) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "zero-length path should have at least 2 points") + assert.Equal(t, 0, points[0][0]) + assert.Equal(t, 0, points[0][1]) + assert.Equal(t, 0, points[len(points)-1][0]) + assert.Equal(t, 0, points[len(points)-1][1]) +} + +func TestHumanizeMouseTrajectory_MaxPointsClampedToMin(t *testing.T) { + // MaxPoints below MinPoints should be clamped up to MinPoints + opts := &Options{MaxPoints: 2} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MinPoints, "MaxPoints below MinPoints should clamp to MinPoints") +} + +func TestHumanizeMouseTrajectory_MaxPointsClampedToMax(t *testing.T) { + // MaxPoints above MaxPoints should be clamped down to MaxPoints + opts := &Options{MaxPoints: 200} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MaxPoints, "MaxPoints above MaxPoints should clamp to MaxPoints") +} + +func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 0, 999) + points := traj.GetPointsInt() + + // For a horizontal move, the Bezier adds control points, so the path may curve + // Middle points should not all lie exactly on the line y=0 (curved path) + require.GreaterOrEqual(t, len(points), 3) + allOnLine := true + for i := 1; i < len(points)-1; i++ { + if points[i][1] != 0 { + allOnLine = false + break + } + } + assert.False(t, allOnLine, "path should be curved, not a straight line") +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index d4d2712b..39e50603 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -115,6 +115,41 @@ const ( Supervisor LogsStreamParamsSource = "supervisor" ) +// AmbientMouseRequest Enable or disable ambient mouse activity. When enabled, the server runs a background +// loop emitting diverse input events (drift, scroll, micro-drag, click, key tap) to +// make the session appear human-like to anti-bot sensors. +type AmbientMouseRequest struct { + // ClickWeight Relative weight for click events. + ClickWeight *int `json:"click_weight,omitempty"` + + // Enabled Whether ambient mouse activity should be active. + Enabled bool `json:"enabled"` + + // KeyTapWeight Relative weight for key tap events. + KeyTapWeight *int `json:"key_tap_weight,omitempty"` + + // MaxIntervalMs Maximum delay in milliseconds between ambient events. + MaxIntervalMs *int `json:"max_interval_ms,omitempty"` + + // MicroDragWeight Relative weight for micro-drag events. + MicroDragWeight *int `json:"micro_drag_weight,omitempty"` + + // MinIntervalMs Minimum delay in milliseconds between ambient events. + MinIntervalMs *int `json:"min_interval_ms,omitempty"` + + // MouseDriftWeight Relative weight for mouse drift events. + MouseDriftWeight *int `json:"mouse_drift_weight,omitempty"` + + // ScrollWeight Relative weight for scroll events. + ScrollWeight *int `json:"scroll_weight,omitempty"` +} + +// AmbientMouseResponse Current state of ambient mouse activity. +type AmbientMouseResponse struct { + // Enabled Whether ambient mouse activity is currently active. + Enabled bool `json:"enabled"` +} + // BatchComputerActionRequest A batch of computer actions to execute sequentially. type BatchComputerActionRequest struct { // Actions Ordered list of actions to execute. Execution stops on the first error. @@ -322,9 +357,15 @@ type MousePositionResponse struct { // MoveMouseRequest defines model for MoveMouseRequest. type MoveMouseRequest struct { + // DurationSec Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. + DurationSec *float32 `json:"duration_sec,omitempty"` + // HoldKeys Modifier keys to hold during the move HoldKeys *[]string `json:"hold_keys,omitempty"` + // Smooth Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + Smooth *bool `json:"smooth,omitempty"` + // X X coordinate to move the cursor to X int `json:"x"` @@ -787,6 +828,9 @@ type PatchChromiumFlagsJSONRequestBody PatchChromiumFlagsJSONBody // UploadExtensionsAndRestartMultipartRequestBody defines body for UploadExtensionsAndRestart for multipart/form-data ContentType. type UploadExtensionsAndRestartMultipartRequestBody UploadExtensionsAndRestartMultipartBody +// SetAmbientMouseJSONRequestBody defines body for SetAmbientMouse for application/json ContentType. +type SetAmbientMouseJSONRequestBody = AmbientMouseRequest + // BatchComputerActionJSONRequestBody defines body for BatchComputerAction for application/json ContentType. type BatchComputerActionJSONRequestBody = BatchComputerActionRequest @@ -952,6 +996,11 @@ type ClientInterface interface { // UploadExtensionsAndRestartWithBody request with any body UploadExtensionsAndRestartWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // SetAmbientMouseWithBody request with any body + SetAmbientMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + SetAmbientMouse(ctx context.Context, body SetAmbientMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // BatchComputerActionWithBody request with any body BatchComputerActionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1165,6 +1214,30 @@ func (c *Client) UploadExtensionsAndRestartWithBody(ctx context.Context, content return c.Client.Do(req) } +func (c *Client) SetAmbientMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetAmbientMouseRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SetAmbientMouse(ctx context.Context, body SetAmbientMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetAmbientMouseRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) BatchComputerActionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewBatchComputerActionRequestWithBody(c.Server, contentType, body) if err != nil { @@ -2038,6 +2111,46 @@ func NewUploadExtensionsAndRestartRequestWithBody(server string, contentType str return req, nil } +// NewSetAmbientMouseRequest calls the generic SetAmbientMouse builder with application/json body +func NewSetAmbientMouseRequest(server string, body SetAmbientMouseJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSetAmbientMouseRequestWithBody(server, "application/json", bodyReader) +} + +// NewSetAmbientMouseRequestWithBody generates requests for SetAmbientMouse with any type of body +func NewSetAmbientMouseRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/ambient_mouse") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewBatchComputerActionRequest calls the generic BatchComputerAction builder with application/json body func NewBatchComputerActionRequest(server string, body BatchComputerActionJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3833,6 +3946,11 @@ type ClientWithResponsesInterface interface { // UploadExtensionsAndRestartWithBodyWithResponse request with any body UploadExtensionsAndRestartWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UploadExtensionsAndRestartResponse, error) + // SetAmbientMouseWithBodyWithResponse request with any body + SetAmbientMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetAmbientMouseResponse, error) + + SetAmbientMouseWithResponse(ctx context.Context, body SetAmbientMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*SetAmbientMouseResponse, error) + // BatchComputerActionWithBodyWithResponse request with any body BatchComputerActionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*BatchComputerActionResponse, error) @@ -4056,6 +4174,30 @@ func (r UploadExtensionsAndRestartResponse) StatusCode() int { return 0 } +type SetAmbientMouseResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *AmbientMouseResponse + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r SetAmbientMouseResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SetAmbientMouseResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type BatchComputerActionResponse struct { Body []byte HTTPResponse *http.Response @@ -5087,6 +5229,23 @@ func (c *ClientWithResponses) UploadExtensionsAndRestartWithBodyWithResponse(ctx return ParseUploadExtensionsAndRestartResponse(rsp) } +// SetAmbientMouseWithBodyWithResponse request with arbitrary body returning *SetAmbientMouseResponse +func (c *ClientWithResponses) SetAmbientMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetAmbientMouseResponse, error) { + rsp, err := c.SetAmbientMouseWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetAmbientMouseResponse(rsp) +} + +func (c *ClientWithResponses) SetAmbientMouseWithResponse(ctx context.Context, body SetAmbientMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*SetAmbientMouseResponse, error) { + rsp, err := c.SetAmbientMouse(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetAmbientMouseResponse(rsp) +} + // BatchComputerActionWithBodyWithResponse request with arbitrary body returning *BatchComputerActionResponse func (c *ClientWithResponses) BatchComputerActionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*BatchComputerActionResponse, error) { rsp, err := c.BatchComputerActionWithBody(ctx, contentType, body, reqEditors...) @@ -5731,6 +5890,46 @@ func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensi return response, nil } +// ParseSetAmbientMouseResponse parses an HTTP response from a SetAmbientMouseWithResponse call +func ParseSetAmbientMouseResponse(rsp *http.Response) (*SetAmbientMouseResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SetAmbientMouseResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest AmbientMouseResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseBatchComputerActionResponse parses an HTTP response from a BatchComputerActionWithResponse call func ParseBatchComputerActionResponse(rsp *http.Response) (*BatchComputerActionResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7377,6 +7576,9 @@ type ServerInterface interface { // Upload one or more unpacked extensions (as zips) and restart Chromium // (POST /chromium/upload-extensions-and-restart) UploadExtensionsAndRestart(w http.ResponseWriter, r *http.Request) + // Enable or disable ambient mouse activity + // (POST /computer/ambient_mouse) + SetAmbientMouse(w http.ResponseWriter, r *http.Request) // Execute a batch of computer actions sequentially // (POST /computer/batch) BatchComputerAction(w http.ResponseWriter, r *http.Request) @@ -7521,6 +7723,12 @@ func (_ Unimplemented) UploadExtensionsAndRestart(w http.ResponseWriter, r *http w.WriteHeader(http.StatusNotImplemented) } +// Enable or disable ambient mouse activity +// (POST /computer/ambient_mouse) +func (_ Unimplemented) SetAmbientMouse(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Execute a batch of computer actions sequentially // (POST /computer/batch) func (_ Unimplemented) BatchComputerAction(w http.ResponseWriter, r *http.Request) { @@ -7810,6 +8018,20 @@ func (siw *ServerInterfaceWrapper) UploadExtensionsAndRestart(w http.ResponseWri handler.ServeHTTP(w, r) } +// SetAmbientMouse operation middleware +func (siw *ServerInterfaceWrapper) SetAmbientMouse(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SetAmbientMouse(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // BatchComputerAction operation middleware func (siw *ServerInterfaceWrapper) BatchComputerAction(w http.ResponseWriter, r *http.Request) { @@ -8787,6 +9009,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/chromium/upload-extensions-and-restart", wrapper.UploadExtensionsAndRestart) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/ambient_mouse", wrapper.SetAmbientMouse) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/batch", wrapper.BatchComputerAction) }) @@ -8993,6 +9218,41 @@ func (response UploadExtensionsAndRestart500JSONResponse) VisitUploadExtensionsA return json.NewEncoder(w).Encode(response) } +type SetAmbientMouseRequestObject struct { + Body *SetAmbientMouseJSONRequestBody +} + +type SetAmbientMouseResponseObject interface { + VisitSetAmbientMouseResponse(w http.ResponseWriter) error +} + +type SetAmbientMouse200JSONResponse AmbientMouseResponse + +func (response SetAmbientMouse200JSONResponse) VisitSetAmbientMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SetAmbientMouse400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response SetAmbientMouse400JSONResponse) VisitSetAmbientMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SetAmbientMouse500JSONResponse struct{ InternalErrorJSONResponse } + +func (response SetAmbientMouse500JSONResponse) VisitSetAmbientMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type BatchComputerActionRequestObject struct { Body *BatchComputerActionJSONRequestBody } @@ -10840,6 +11100,9 @@ type StrictServerInterface interface { // Upload one or more unpacked extensions (as zips) and restart Chromium // (POST /chromium/upload-extensions-and-restart) UploadExtensionsAndRestart(ctx context.Context, request UploadExtensionsAndRestartRequestObject) (UploadExtensionsAndRestartResponseObject, error) + // Enable or disable ambient mouse activity + // (POST /computer/ambient_mouse) + SetAmbientMouse(ctx context.Context, request SetAmbientMouseRequestObject) (SetAmbientMouseResponseObject, error) // Execute a batch of computer actions sequentially // (POST /computer/batch) BatchComputerAction(ctx context.Context, request BatchComputerActionRequestObject) (BatchComputerActionResponseObject, error) @@ -11059,6 +11322,37 @@ func (sh *strictHandler) UploadExtensionsAndRestart(w http.ResponseWriter, r *ht } } +// SetAmbientMouse operation middleware +func (sh *strictHandler) SetAmbientMouse(w http.ResponseWriter, r *http.Request) { + var request SetAmbientMouseRequestObject + + var body SetAmbientMouseJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SetAmbientMouse(ctx, request.(SetAmbientMouseRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SetAmbientMouse") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SetAmbientMouseResponseObject); ok { + if err := validResponse.VisitSetAmbientMouseResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // BatchComputerAction operation middleware func (sh *strictHandler) BatchComputerAction(w http.ResponseWriter, r *http.Request) { var request BatchComputerActionRequestObject @@ -12298,144 +12592,156 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "i7R+/eBG/l8AAAD//yIMPJrxsgAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index b0bc70d9..d9d2d6db 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -502,6 +502,38 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /computer/ambient_mouse: + post: + summary: Enable or disable ambient mouse activity + description: | + Toggle a background loop that emits diverse input events (mouse drift, scroll, + micro-drag, click, key tap) to make the browser session appear more human-like to + anti-bot sensors. + + When enabled, the loop acquires the same input lock used by all other computer-use + APIs, so ambient events never interleave with explicit actions. The loop automatically + yields the lock between events, allowing explicit API calls to take priority. + + Call with enabled=false to stop the loop. Calling with enabled=true while already + running replaces the configuration (restarts the loop). + operationId: setAmbientMouse + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AmbientMouseRequest" + responses: + "200": + description: Ambient mouse state updated + content: + application/json: + schema: + $ref: "#/components/schemas/AmbientMouseResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" /logs/stream: get: summary: Stream logs over SSE @@ -1232,6 +1264,15 @@ components: description: Modifier keys to hold during the move items: type: string + smooth: + type: boolean + description: Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + default: true + duration_sec: + type: number + description: Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. + minimum: 0.05 + maximum: 5 additionalProperties: false ScreenshotRegion: type: object @@ -1822,6 +1863,64 @@ components: items: $ref: "#/components/schemas/ComputerAction" additionalProperties: false + AmbientMouseRequest: + type: object + description: | + Enable or disable ambient mouse activity. When enabled, the server runs a background + loop emitting diverse input events (drift, scroll, micro-drag, click, key tap) to + make the session appear human-like to anti-bot sensors. + required: [enabled] + properties: + enabled: + type: boolean + description: Whether ambient mouse activity should be active. + min_interval_ms: + type: integer + description: Minimum delay in milliseconds between ambient events. + minimum: 50 + maximum: 10000 + default: 200 + max_interval_ms: + type: integer + description: Maximum delay in milliseconds between ambient events. + minimum: 50 + maximum: 30000 + default: 600 + mouse_drift_weight: + type: integer + description: Relative weight for mouse drift events. + minimum: 0 + default: 55 + scroll_weight: + type: integer + description: Relative weight for scroll events. + minimum: 0 + default: 20 + micro_drag_weight: + type: integer + description: Relative weight for micro-drag events. + minimum: 0 + default: 12 + click_weight: + type: integer + description: Relative weight for click events. + minimum: 0 + default: 10 + key_tap_weight: + type: integer + description: Relative weight for key tap events. + minimum: 0 + default: 3 + additionalProperties: false + AmbientMouseResponse: + type: object + description: Current state of ambient mouse activity. + required: [enabled] + properties: + enabled: + type: boolean + description: Whether ambient mouse activity is currently active. + additionalProperties: false responses: BadRequestError: description: Bad Request