Skip to content

Commit 513303d

Browse files
Replace uniform mouse jitter with Gaussian-distributed delays
The per-step delay jitter for smooth mouse movements and drags used a uniform ±2ms offset (rand.Intn(5) - 2), producing near-constant inter-event timing. Real human mouse movement has substantially more timing variance due to motor noise, acceleration phases, and micro-hesitations. Replace with a Gaussian distribution (Box-Muller, stddev = 40% of mean delay, clamped to [3ms, 3x mean]). This preserves the average movement duration while producing natural timing variance in each step. Affects both doMoveMouseSmooth and doDragMouseSmooth code paths. Made-with: Cursor
1 parent 5a2b521 commit 513303d

2 files changed

Lines changed: 157 additions & 15 deletions

File tree

server/cmd/api/api/computer.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo
173173
}()
174174
}
175175

176-
// Move along Bezier path: mousemove_relative for each step with delay
176+
// Move along Bezier path: mousemove_relative for each step with delay.
177+
// Use Gaussian-distributed delays so that inter-event timing has natural
178+
// variance matching real human motor noise, rather than near-constant
179+
// intervals that fingerprinting systems can distinguish from humans.
177180
for i := 1; i < len(points); i++ {
178181
select {
179182
case <-ctx.Done():
@@ -190,14 +193,8 @@ func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, bo
190193
return &executionError{msg: "failed during smooth mouse movement"}
191194
}
192195
}
193-
jitter := stepDelayMs
194-
if stepDelayMs > 3 {
195-
jitter = stepDelayMs + rand.Intn(5) - 2
196-
if jitter < 3 {
197-
jitter = 3
198-
}
199-
}
200-
if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil {
196+
delay := gaussianDelay(stepDelayMs, 3)
197+
if err := sleepWithContext(ctx, time.Duration(delay)*time.Millisecond); err != nil {
201198
return &executionError{msg: "mouse movement cancelled"}
202199
}
203200
}
@@ -1233,12 +1230,9 @@ func (s *ApiService) doDragMouseSmooth(ctx context.Context, log *slog.Logger, bo
12331230
args = append(args, "mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy))
12341231

12351232
if i < numSteps {
1236-
delay := smoothStepDelay(i, numSteps, baseDelayMs*2, baseDelayMs/2)
1237-
jitter := delay + rand.Intn(5) - 2
1238-
if jitter < 3 {
1239-
jitter = 3
1240-
}
1241-
args = append(args, "sleep", fmt.Sprintf("%.3f", float64(jitter)/1000.0))
1233+
baseDelay := smoothStepDelay(i, numSteps, baseDelayMs*2, baseDelayMs/2)
1234+
delay := gaussianDelay(baseDelay, 3)
1235+
args = append(args, "sleep", fmt.Sprintf("%.3f", float64(delay)/1000.0))
12421236
}
12431237
}
12441238

@@ -1254,6 +1248,28 @@ func (s *ApiService) doDragMouseSmooth(ctx context.Context, log *slog.Logger, bo
12541248
return nil
12551249
}
12561250

1251+
// gaussianDelay returns a Gaussian-distributed delay centered on meanMs with
1252+
// stddev of 40% of meanMs, clamped to [minMs, 3*meanMs]. This produces timing
1253+
// variance that matches real human motor noise rather than the near-zero
1254+
// variance of uniform jitter.
1255+
func gaussianDelay(meanMs int, minMs int) int {
1256+
stddev := float64(meanMs) * 0.4
1257+
u1 := rand.Float64()
1258+
u2 := rand.Float64()
1259+
if u1 <= 0 {
1260+
u1 = 1e-10
1261+
}
1262+
z := math.Sqrt(-2*math.Log(u1)) * math.Cos(2*math.Pi*u2)
1263+
delay := int(math.Round(float64(meanMs) + stddev*z))
1264+
if delay < minMs {
1265+
delay = minMs
1266+
}
1267+
if delay > meanMs*3 {
1268+
delay = meanMs * 3
1269+
}
1270+
return delay
1271+
}
1272+
12571273
// smoothStepDelay maps position i/n through a smoothstep curve to produce
12581274
// a delay in [fastMs, slowMs]. Slow at start and end, fast in the middle.
12591275
// smoothstep(t) = 3t² - 2t³

server/cmd/api/api/computer_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package api
33
import (
44
"errors"
55
"fmt"
6+
"math"
7+
"math/rand"
68
"testing"
79

810
"github.com/stretchr/testify/assert"
@@ -168,6 +170,130 @@ func TestIsValidationErr_Nil(t *testing.T) {
168170
assert.False(t, isValidationErr(nil))
169171
}
170172

173+
func TestGaussianDelay(t *testing.T) {
174+
const n = 10000
175+
meanMs := 10
176+
177+
var sum, sumSq float64
178+
minVal, maxVal := math.MaxFloat64, -math.MaxFloat64
179+
180+
for i := 0; i < n; i++ {
181+
d := float64(gaussianDelay(meanMs, 3))
182+
sum += d
183+
sumSq += d * d
184+
if d < minVal {
185+
minVal = d
186+
}
187+
if d > maxVal {
188+
maxVal = d
189+
}
190+
}
191+
192+
avg := sum / n
193+
variance := sumSq/n - avg*avg
194+
195+
assert.InDelta(t, float64(meanMs), avg, float64(meanMs)*0.15,
196+
"average delay should be near %dms, got %.1fms", meanMs, avg)
197+
198+
assert.Greater(t, variance, 5.0,
199+
"variance should be substantial for human-like timing, got %.1f", variance)
200+
201+
assert.GreaterOrEqual(t, minVal, 3.0, "delay must not go below floor")
202+
203+
assert.LessOrEqual(t, maxVal, float64(meanMs*3), "delay must not exceed 3x mean")
204+
}
205+
206+
func TestGaussianDelay_VarianceMuchHigherThanUniform(t *testing.T) {
207+
const n = 5000
208+
meanMs := 10
209+
210+
var gSum, gSumSq float64
211+
for i := 0; i < n; i++ {
212+
d := float64(gaussianDelay(meanMs, 3))
213+
gSum += d
214+
gSumSq += d * d
215+
}
216+
gAvg := gSum / n
217+
gVariance := gSumSq/n - gAvg*gAvg
218+
219+
// Old uniform: meanMs + rand.Intn(5) - 2, variance of {-2,-1,0,1,2} = 2.0
220+
uniformVariance := 2.0
221+
222+
assert.Greater(t, gVariance, uniformVariance*3,
223+
"Gaussian variance (%.1f) should be much larger than old uniform variance (%.1f)",
224+
gVariance, uniformVariance)
225+
}
226+
227+
// welford implements Welford's online algorithm for computing running variance.
228+
// This is the same algorithm used by browser fingerprinting systems to evaluate
229+
// whether mouse movement timing looks human or automated.
230+
type welford struct {
231+
n int
232+
mean float64
233+
m2 float64
234+
}
235+
236+
func (w *welford) add(v float64) {
237+
w.n++
238+
delta := v - w.mean
239+
w.mean += delta / float64(w.n)
240+
w.m2 += delta * (v - w.mean)
241+
}
242+
243+
func (w *welford) variance() float64 {
244+
if w.n < 2 {
245+
return 0
246+
}
247+
return w.m2 / float64(w.n-1)
248+
}
249+
250+
func TestGaussianDelay_WelfordVelocityVariance(t *testing.T) {
251+
// Simulate a mouse trajectory: 50 points with varying pixel distances
252+
// (as produced by a Bezier curve), timed with gaussianDelay intervals.
253+
// Compute velocity = distance / delay for each step and measure Welford
254+
// variance. Human-like velocity variance should be well above 5.
255+
const steps = 50
256+
meanDelayMs := 10
257+
258+
// Pixel distances per step typical of a Bezier curve across ~400px.
259+
// Real trajectories vary: small moves near endpoints, larger in the middle.
260+
rng := rand.New(rand.NewSource(42))
261+
distances := make([]float64, steps)
262+
for i := range distances {
263+
t_norm := float64(i) / float64(steps)
264+
base := 5.0 + 15.0*math.Sin(t_norm*math.Pi) // 5-20px, peaked in middle
265+
distances[i] = base + rng.Float64()*3.0 // small random variation
266+
}
267+
268+
// Gaussian delays → velocity variance
269+
var gaussianVelVar welford
270+
for i := 0; i < steps; i++ {
271+
delay := float64(gaussianDelay(meanDelayMs, 3))
272+
velocity := distances[i] / delay
273+
gaussianVelVar.add(velocity)
274+
}
275+
276+
// Uniform delays (old approach) → velocity variance
277+
var uniformVelVar welford
278+
for i := 0; i < steps; i++ {
279+
delay := float64(meanDelayMs + rand.Intn(5) - 2)
280+
if delay < 3 {
281+
delay = 3
282+
}
283+
velocity := distances[i] / delay
284+
uniformVelVar.add(velocity)
285+
}
286+
287+
t.Logf("Gaussian velocity variance: %.4f (n=%d)", gaussianVelVar.variance(), gaussianVelVar.n)
288+
t.Logf("Uniform velocity variance: %.4f (n=%d)", uniformVelVar.variance(), uniformVelVar.n)
289+
290+
assert.Greater(t, gaussianVelVar.variance(), uniformVelVar.variance(),
291+
"Gaussian timing should produce higher velocity variance than uniform")
292+
293+
assert.Greater(t, gaussianVelVar.variance(), 0.05,
294+
"Gaussian velocity variance should be well above near-zero")
295+
}
296+
171297
func TestClampPoints(t *testing.T) {
172298
tests := []struct {
173299
name string

0 commit comments

Comments
 (0)