Skip to content

Commit 3f19e61

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 3f19e61

2 files changed

Lines changed: 86 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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"errors"
55
"fmt"
6+
"math"
67
"testing"
78

89
"github.com/stretchr/testify/assert"
@@ -168,6 +169,60 @@ func TestIsValidationErr_Nil(t *testing.T) {
168169
assert.False(t, isValidationErr(nil))
169170
}
170171

172+
func TestGaussianDelay(t *testing.T) {
173+
const n = 10000
174+
meanMs := 10
175+
176+
var sum, sumSq float64
177+
minVal, maxVal := math.MaxFloat64, -math.MaxFloat64
178+
179+
for i := 0; i < n; i++ {
180+
d := float64(gaussianDelay(meanMs, 3))
181+
sum += d
182+
sumSq += d * d
183+
if d < minVal {
184+
minVal = d
185+
}
186+
if d > maxVal {
187+
maxVal = d
188+
}
189+
}
190+
191+
avg := sum / n
192+
variance := sumSq/n - avg*avg
193+
194+
assert.InDelta(t, float64(meanMs), avg, float64(meanMs)*0.15,
195+
"average delay should be near %dms, got %.1fms", meanMs, avg)
196+
197+
assert.Greater(t, variance, 5.0,
198+
"variance should be substantial for human-like timing, got %.1f", variance)
199+
200+
assert.GreaterOrEqual(t, minVal, 3.0, "delay must not go below floor")
201+
202+
assert.LessOrEqual(t, maxVal, float64(meanMs*3), "delay must not exceed 3x mean")
203+
}
204+
205+
func TestGaussianDelay_VarianceMuchHigherThanUniform(t *testing.T) {
206+
const n = 5000
207+
meanMs := 10
208+
209+
var gSum, gSumSq float64
210+
for i := 0; i < n; i++ {
211+
d := float64(gaussianDelay(meanMs, 3))
212+
gSum += d
213+
gSumSq += d * d
214+
}
215+
gAvg := gSum / n
216+
gVariance := gSumSq/n - gAvg*gAvg
217+
218+
// Old uniform: meanMs + rand.Intn(5) - 2, variance of {-2,-1,0,1,2} = 2.0
219+
uniformVariance := 2.0
220+
221+
assert.Greater(t, gVariance, uniformVariance*3,
222+
"Gaussian variance (%.1f) should be much larger than old uniform variance (%.1f)",
223+
gVariance, uniformVariance)
224+
}
225+
171226
func TestClampPoints(t *testing.T) {
172227
tests := []struct {
173228
name string

0 commit comments

Comments
 (0)