From f508bfaaf8a96f765c4c2bf1c3d5a22880a43a2f Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Mon, 23 Feb 2026 15:01:58 -0700 Subject: [PATCH 1/5] Add plan for humanizing all computer interaction endpoints Covers click, type, press key, scroll, and drag mouse with performance-first algorithms (zero additional xdotool process spawns). Includes the existing Bezier curve mouse movement as reference. Co-authored-by: Cursor --- plans/humanize-computer-endpoints.md | 210 +++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 plans/humanize-computer-endpoints.md diff --git a/plans/humanize-computer-endpoints.md b/plans/humanize-computer-endpoints.md new file mode 100644 index 00000000..2eb8962c --- /dev/null +++ b/plans/humanize-computer-endpoints.md @@ -0,0 +1,210 @@ +# Humanize All Computer Interaction Endpoints + +> Add human-like behavior to all computer interaction API endpoints using fast, pre-computed algorithms that add zero additional xdotool process spawns. + +## Performance-First Design Principle + +**The bottleneck is xdotool process spawns** (fork+exec per call), not Go-side computation. Every algorithm below is designed around two rules: + +1. **One xdotool call per API request** -- pre-compute all timing in Go and bake it into a single chained xdotool command with inline `sleep` directives. This is the same pattern already used by `doDragMouse` (see lines 911-951 of `computer.go`). +2. **O(1) or O(n) math only** -- uniform random (`rand.Intn`), simple easing polynomials (2-3 multiplies), no lookup tables, no transcendental functions beyond what `mousetrajectory.go` already uses. + +```mermaid +flowchart LR + Go["Go: pre-compute timing array O(n)"] --> Args["Build xdotool arg slice"] + Args --> OneExec["Single fork+exec"] + OneExec --> Done["Done"] +``` + + + +### Existing proof this works + +`doDragMouse` already chains `mousemove_relative dx dy sleep 0.050 mousemove_relative dx dy sleep 0.050 ...` in a single xdotool invocation. Every strategy below follows this exact pattern. + +--- + +## 0. Move Mouse -- Bezier Curve Trajectory (Already Implemented) + +**Status:** Complete. This is the reference implementation that all other endpoints follow. + +**Cost:** N xdotool calls (one `mousemove_relative` per trajectory point) with Go-side sleeps. Typically 5-80 steps depending on distance. + +**Algorithm:** Bezier curve with randomized control points, distortion, and easing. Ported from Camoufox/HumanCursor. + +- **Bezier curve**: 2 random internal knots within an 80px-padded bounding box around start/end. Bernstein polynomial evaluation produces smooth curved path. O(n) computation. +- **Distortion**: 50% chance per interior point to apply Gaussian jitter (mean=1, stdev=1 via Box-Muller transform). Adds micro-imperfections. +- **Easing**: `easeOutQuad(t) = -t*(t-2)` -- cursor decelerates as it approaches the target, matching natural human behavior. +- **Point count**: Auto-computed from path length (`pathLength^0.25 * 20`), clamped to [5, 80]. Override via `Options.MaxPoints`. +- **Per-step timing**: ~10ms default step delay with +/-2ms uniform jitter. When `duration_ms` is specified, delay is computed as `duration_ms / numSteps`. +- **Screen clamping**: Trajectory points clamped to screen bounds to prevent X11 delta accumulation errors. + +**Key files:** + +- `[server/lib/mousetrajectory/mousetrajectory.go](kernel-images/server/lib/mousetrajectory/mousetrajectory.go)` -- Bezier curve generation (~230 lines) +- `[server/cmd/api/api/computer.go](kernel-images/server/cmd/api/api/computer.go)` lines 104-206 -- `doMoveMouseSmooth` integration + +**API (existing):** `MoveMouseRequest` has `smooth: boolean` (default `true`) and optional `duration_ms` (50-5000ms). + +**Implementation in `doMoveMouseSmooth`:** + +1. Get current mouse position via `xdotool getmouselocation` +2. Generate Bezier trajectory: `mousetrajectory.NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY, opts)` +3. Clamp points to screen bounds +4. For each point: `xdotool mousemove_relative -- dx dy`, then `sleepWithContext` with jittered delay +5. Modifier keys held via `keydown`/`keyup` wrapper + +**Note:** This endpoint uses per-step Go-side sleeps (not xdotool inline `sleep`) because the trajectory includes screen-clamping logic that adjusts deltas at runtime. The other endpoints below use inline `sleep` since their timing can be fully pre-computed. + +--- + +## Shared Library: `server/lib/humanize/humanize.go` + +Tiny utility package (no external deps, no data structures) providing: + +```go +// UniformJitter returns a random duration in [base-jitter, base+jitter], clamped to min. +func UniformJitter(rng *rand.Rand, baseMs, jitterMs, minMs int) time.Duration + +// EaseOutQuad computes t*(2-t) for t in [0,1]. Two multiplies. +func EaseOutQuad(t float64) float64 + +// SmoothStepDelay maps position i/n through a smoothstep curve to produce +// a delay in [fastMs, slowMs]. Used for scroll and drag easing. +// smoothstep(t) = 3t^2 - 2t^3. Three multiplies. +func SmoothStepDelay(i, n, slowMs, fastMs int) time.Duration + +// FormatSleepArg formats a duration as a string suitable for xdotool's +// inline sleep command (e.g. "0.085"). Avoids fmt.Sprintf per call. +func FormatSleepArg(d time.Duration) string +``` + +All functions are pure, allocate nothing, and cost a few arithmetic ops each. Tested with table-driven tests and deterministic seeds. + +--- + +## 1. Click Mouse -- Single-Call Down/Sleep/Up + +**Cost:** 1 xdotool call (same as current). Pre-computation: 1-2 `rand.Intn` calls. + +**Algorithm:** Replace `click` with `mousedown sleep mouseup ` in the same xdotool arg slice. No separate process spawns. + +- **Dwell time**: `UniformJitter(rng, 90, 30, 50)` -> range [60, 120]ms. This matches measured human click dwell without needing lognormal sampling. +- **Micro-drift**: Append `mousemove_relative ` between mousedown and mouseup, where dx/dy are `rand.Intn(3)-1` (range [-1, 1] pixels). Trivially cheap. +- **Multi-click**: For `num_clicks > 1`, loop and insert inter-click gaps via `UniformJitter(rng, 100, 30, 60)` -> [70, 130]ms. + +**Single xdotool call example:** + +``` +xdotool mousemove 500 300 mousedown 1 sleep 0.085 mousemove_relative -- 1 0 mouseup 1 +``` + +**API change:** Add `smooth: boolean` (default `true`) to `ClickMouseRequest`. + +--- + +## 2. Type Text -- Chunked Type with Inter-Word Pauses + +**Cost:** 1 xdotool call (same as current). Pre-computation: O(words) random samples. + +**Algorithm:** Instead of per-character keysym mapping (which is complex and fragile for Unicode), split text by whitespace/punctuation into chunks and chain `xdotool type --delay "chunk" sleep ` commands. + +- **Intra-word delay**: Per-chunk, pick `rand.Intn(70) + 50` -> [50, 120]ms. Varies per chunk to simulate burst-pause rhythm. +- **Inter-word pause**: Between chunks, insert `sleep` with `UniformJitter(rng, 140, 60, 60)` -> [80, 200]ms. Longer pauses at sentence boundaries (after `.!?`): multiply by 1.5x. +- **No bigram tables**: The per-word delay variation is sufficient for convincing humanization. Bigram-level precision adds complexity with diminishing returns for bot detection evasion. + +**Single xdotool call example:** + +``` +xdotool type --delay 80 -- "Hello" sleep 0.150 type --delay 65 -- " world" sleep 0.300 type --delay 95 -- ". How" sleep 0.120 type --delay 70 -- " are" sleep 0.140 type --delay 85 -- " you?" +``` + +**API change:** Add `smooth: boolean` (default `false`) to `TypeTextRequest`. When `smooth=true`, the existing `delay` field is ignored. + +**Why this is fast:** We never leave the `xdotool type` mechanism (which handles Unicode, XKB keymaps, etc. internally). We just break it into chunks with sleeps between them. One fork+exec total. + +--- + +## 3. Press Key -- Dwell via Inline Sleep + +**Cost:** 1 xdotool call (same as current). Pre-computation: 1 `rand.Intn` call. + +**Algorithm:** Replace `key ` with `keydown sleep keyup `. + +- **Tap dwell**: `UniformJitter(rng, 95, 30, 50)` -> [65, 125]ms. +- **Modifier stagger**: When `hold_keys` are present, insert a small `sleep 0.025` between each `keydown` for modifiers, then the primary key sequence. Release in reverse order with the same stagger. This costs zero extra xdotool calls -- it's all in the same arg slice. + +**Single xdotool call example (Ctrl+C):** + +``` +xdotool keydown ctrl sleep 0.030 keydown c sleep 0.095 keyup c sleep 0.025 keyup ctrl +``` + +**API change:** Add `smooth: boolean` (default `false`) to `PressKeyRequest`. + +--- + +## 4. Scroll -- Eased Tick Intervals in One Call + +**Cost:** 1 xdotool call (same as current). Pre-computation: O(ticks) easing function evaluations (3 multiplies each). + +**Algorithm:** Replace `click --repeat N --delay 0 ` with N individual `click ` commands separated by pre-computed `sleep` values following a **smoothstep easing curve**. + +- **Easing**: `SmoothStepDelay(i, N, slowMs=80, fastMs=15)` for each tick i. The smoothstep `3t^2 - 2t^3` creates natural momentum: slow start, fast middle, slow end. +- **Jitter**: Add `rand.Intn(10) - 5` ms to each delay. Trivially cheap. +- **Small scrolls (1-3 ticks)**: Skip easing, use uniform delay of `rand.Intn(40) + 30` ms. + +**Single xdotool call example (5 ticks down):** + +``` +xdotool mousemove 500 300 click 5 sleep 0.075 click 5 sleep 0.035 click 5 sleep 0.018 click 5 sleep 0.040 click 5 +``` + +**API change:** Add `smooth: boolean` (default `false`) to `ScrollRequest`. + +**Why not per-tick Go-side sleeps?** That would require N separate xdotool calls (N fork+execs). Inline `sleep` achieves the same timing in one process. + +--- + +## 5. Drag Mouse -- Bezier Path + Eased Delays + +**Cost:** Same as current (1-3 xdotool calls for the 3 phases). Pre-computation: Bezier generation (already proven fast in `mousetrajectory.go`). + +**Algorithm:** When `smooth=true`, auto-generate the drag path using the existing `mousetrajectory.HumanizeMouseTrajectory` Bezier library, then apply eased step delays (instead of the current fixed `step_delay_ms`). + +- **Path generation**: `mousetrajectory.NewHumanizeMouseTrajectoryWithOptions(startX, startY, endX, endY, opts)` -- already O(n) with Bernstein polynomial evaluation. Proven fast. +- **Eased step delays**: Replace the fixed `stepDelaySeconds` in the Phase 2 xdotool chain with per-step delays from `SmoothStepDelay`. Slow at start (pickup) and end (placement), fast in middle. These are already baked into the single xdotool arg slice, so zero extra process spawns. +- **Jitter**: Same `rand.Intn(5) - 2` ms pattern already used by `doMoveMouseSmooth`. + +**API change:** Add `smooth: boolean` (default `false`) to `DragMouseRequest`. When `smooth=true` and `path` has exactly 2 points (start + end), the server generates a Bezier curve between them and replaces `path` with the generated waypoints. + +**No new `start`/`end` fields needed** -- the caller simply provides `path: [[startX, startY], [endX, endY]]` and the server expands it. + +--- + +## Computational Cost Summary + + +| Endpoint | xdotool calls | Pre-computation | Algorithm | +| ------------- | ---------------- | --------------------------------- | ------------------------------------ | +| `move_mouse` | O(points) (done) | O(points) Bezier + Box-Muller | Bezier curve + easeOutQuad + jitter | +| `click_mouse` | 1 (same) | 1-2x `rand.Intn` | Uniform random dwell | +| `type_text` | 1 (same) | O(words) `rand.Intn` | Chunked type + inter-word sleep | +| `press_key` | 1 (same) | 1x `rand.Intn` | Inline keydown/sleep/keyup | +| `scroll` | 1 (same) | O(ticks) smoothstep (3 muls each) | Eased inter-tick sleep | +| `drag_mouse` | 1-3 (same) | O(points) Bezier (existing) | Bezier path + smoothstep step delays | + + +No additional process spawns. No heap allocations beyond the existing xdotool arg slice. No lookup tables. Every random sample is a single `rand.Intn` or `rand.Float64` call. + +--- + +## Files to Create/Modify + +- **Modify:** `[server/openapi.yaml](kernel-images/server/openapi.yaml)` -- Add `smooth` boolean to 5 request schemas +- **Modify:** `[server/cmd/api/api/computer.go](kernel-images/server/cmd/api/api/computer.go)` -- Add humanized code paths (branching on `smooth` flag) +- **Create:** `server/lib/humanize/humanize.go` -- Shared primitives (~50 lines) +- **Create:** `server/lib/humanize/humanize_test.go` -- Table-driven tests +- **Regenerate:** OpenAPI-generated types (run code generation after schema changes) + +No separate per-endpoint library packages needed. The shared `humanize` package plus the existing `mousetrajectory` package cover everything. \ No newline at end of file From 627054d49098fa49c10f380af55ba18cb7827e56 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Tue, 24 Mar 2026 13:55:55 -0600 Subject: [PATCH 2/5] Address review feedback on humanize plan - Type text: use O(words) separate xdotool calls with Go-side sleeps instead of single-call chaining, since xdotool type consumes rest of argv and can't be chained with sleep. - Type text: keep trailing delimiters (space, punctuation) with the preceding chunk so pauses happen after word boundaries. - Scroll: use bounded total duration (default 200ms) instead of fixed per-tick slowMs/fastMs, so large tick counts don't block input. Made-with: Cursor --- plans/humanize-computer-endpoints.md | 34 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/plans/humanize-computer-endpoints.md b/plans/humanize-computer-endpoints.md index 2eb8962c..c0f1177b 100644 --- a/plans/humanize-computer-endpoints.md +++ b/plans/humanize-computer-endpoints.md @@ -105,23 +105,32 @@ xdotool mousemove 500 300 mousedown 1 sleep 0.085 mousemove_relative -- 1 0 mous ## 2. Type Text -- Chunked Type with Inter-Word Pauses -**Cost:** 1 xdotool call (same as current). Pre-computation: O(words) random samples. +**Cost:** O(words) xdotool calls with Go-side sleeps. Pre-computation: O(words) random samples. -**Algorithm:** Instead of per-character keysym mapping (which is complex and fragile for Unicode), split text by whitespace/punctuation into chunks and chain `xdotool type --delay "chunk" sleep ` commands. +**Algorithm:** Split text into word chunks (keeping trailing whitespace/punctuation with each chunk), then issue one `xdotool type --delay -- ""` call per chunk with Go-side `sleepWithContext` pauses between them. This follows the same pattern as `doMoveMouseSmooth` (O(n) calls with Go-side sleeps). +- **Chunking**: Split at word boundaries, keeping trailing delimiters (spaces, punctuation) attached to the preceding chunk. `"Hello world. How are you?"` becomes `["Hello ", "world. ", "How ", "are ", "you?"]`. This ensures pauses happen *after* typing the delimiter, matching natural rhythm. - **Intra-word delay**: Per-chunk, pick `rand.Intn(70) + 50` -> [50, 120]ms. Varies per chunk to simulate burst-pause rhythm. -- **Inter-word pause**: Between chunks, insert `sleep` with `UniformJitter(rng, 140, 60, 60)` -> [80, 200]ms. Longer pauses at sentence boundaries (after `.!?`): multiply by 1.5x. +- **Inter-word pause**: Between chunks, Go-side sleep with `UniformJitter(rng, 140, 60, 60)` -> [80, 200]ms. Longer pauses at sentence boundaries (chunk ends with `.!?`): multiply by 1.5x. - **No bigram tables**: The per-word delay variation is sufficient for convincing humanization. Bigram-level precision adds complexity with diminishing returns for bot detection evasion. -**Single xdotool call example:** +**Execution sequence example (`"Hello world. How are you?"`):** ``` -xdotool type --delay 80 -- "Hello" sleep 0.150 type --delay 65 -- " world" sleep 0.300 type --delay 95 -- ". How" sleep 0.120 type --delay 70 -- " are" sleep 0.140 type --delay 85 -- " you?" +xdotool type --delay 80 -- "Hello " # fork+exec 1 + [Go sleep 150ms] +xdotool type --delay 65 -- "world. " # fork+exec 2 + [Go sleep 300ms] # sentence boundary: 1.5x pause +xdotool type --delay 95 -- "How " # fork+exec 3 + [Go sleep 120ms] +xdotool type --delay 70 -- "are " # fork+exec 4 + [Go sleep 140ms] +xdotool type --delay 85 -- "you?" # fork+exec 5 ``` **API change:** Add `smooth: boolean` (default `false`) to `TypeTextRequest`. When `smooth=true`, the existing `delay` field is ignored. -**Why this is fast:** We never leave the `xdotool type` mechanism (which handles Unicode, XKB keymaps, etc. internally). We just break it into chunks with sleeps between them. One fork+exec total. +**Why O(words) calls, not 1?** `xdotool type` consumes the rest of argv as text to type, so it cannot be chained with `sleep` or other commands in a single invocation. O(words) fork+execs (typically 5-15 for a sentence) is acceptable -- the inter-word pauses (80-300ms) dwarf the ~1-2ms fork+exec overhead. --- @@ -150,14 +159,15 @@ xdotool keydown ctrl sleep 0.030 keydown c sleep 0.095 keyup c sleep 0.025 keyup **Algorithm:** Replace `click --repeat N --delay 0 ` with N individual `click ` commands separated by pre-computed `sleep` values following a **smoothstep easing curve**. -- **Easing**: `SmoothStepDelay(i, N, slowMs=80, fastMs=15)` for each tick i. The smoothstep `3t^2 - 2t^3` creates natural momentum: slow start, fast middle, slow end. -- **Jitter**: Add `rand.Intn(10) - 5` ms to each delay. Trivially cheap. -- **Small scrolls (1-3 ticks)**: Skip easing, use uniform delay of `rand.Intn(40) + 30` ms. +- **Bounded total duration**: Target a fixed total scroll time regardless of tick count. Default `totalMs = 200` (capped so large scrolls don't block input). Per-tick delay = `totalMs / N`, then shaped by the easing curve. +- **Easing**: `SmoothStepDelay(i, N, slowMs, fastMs)` where `slowMs` and `fastMs` are derived from `totalMs / N`. The smoothstep `3t^2 - 2t^3` creates natural momentum: slow start, fast middle, slow end. Edge delays are ~2x the center delay. +- **Jitter**: Add `rand.Intn(6) - 3` ms to each delay. Trivially cheap. +- **Small scrolls (1-3 ticks)**: Skip easing, use uniform delay of `rand.Intn(20) + 10` ms. -**Single xdotool call example (5 ticks down):** +**Single xdotool call example (5 ticks down, totalMs=200, avg 40ms/tick):** ``` -xdotool mousemove 500 300 click 5 sleep 0.075 click 5 sleep 0.035 click 5 sleep 0.018 click 5 sleep 0.040 click 5 +xdotool mousemove 500 300 click 5 sleep 0.055 click 5 sleep 0.030 click 5 sleep 0.025 click 5 sleep 0.032 click 5 ``` **API change:** Add `smooth: boolean` (default `false`) to `ScrollRequest`. @@ -189,7 +199,7 @@ xdotool mousemove 500 300 click 5 sleep 0.075 click 5 sleep 0.035 click 5 sleep | ------------- | ---------------- | --------------------------------- | ------------------------------------ | | `move_mouse` | O(points) (done) | O(points) Bezier + Box-Muller | Bezier curve + easeOutQuad + jitter | | `click_mouse` | 1 (same) | 1-2x `rand.Intn` | Uniform random dwell | -| `type_text` | 1 (same) | O(words) `rand.Intn` | Chunked type + inter-word sleep | +| `type_text` | O(words) | O(words) `rand.Intn` | Per-word type + Go-side sleep | | `press_key` | 1 (same) | 1x `rand.Intn` | Inline keydown/sleep/keyup | | `scroll` | 1 (same) | O(ticks) smoothstep (3 muls each) | Eased inter-tick sleep | | `drag_mouse` | 1-3 (same) | O(points) Bezier (existing) | Bezier path + smoothstep step delays | From 464b9e377f0c80f749b8358ef373b6fce4d814e6 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Fri, 3 Apr 2026 11:02:08 -0600 Subject: [PATCH 3/5] Add typo injection section to humanize plan Introduces optional typo_chance parameter for realistic typing errors. Uses geometric gap sampling (O(typos) random calls, not O(chars)) with QWERTY adjacency lookup for adjacent-key substitution. Includes correction sequence with "realization" pause before backspace. Made-with: Cursor --- plans/humanize-computer-endpoints.md | 65 +++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/plans/humanize-computer-endpoints.md b/plans/humanize-computer-endpoints.md index c0f1177b..f9f5bfaa 100644 --- a/plans/humanize-computer-endpoints.md +++ b/plans/humanize-computer-endpoints.md @@ -132,6 +132,69 @@ xdotool type --delay 85 -- "you?" # fork+exec 5 **Why O(words) calls, not 1?** `xdotool type` consumes the rest of argv as text to type, so it cannot be chained with `sleep` or other commands in a single invocation. O(words) fork+execs (typically 5-15 for a sentence) is acceptable -- the inter-word pauses (80-300ms) dwarf the ~1-2ms fork+exec overhead. +### 2a. Typo Injection (Optional) + +Optionally introduce realistic typos that are immediately corrected with backspace, simulating natural human typing errors. + +**Research basis:** The [136 million keystrokes study (Aalto/CHI 2018)](https://userinterfaces.aalto.fi/136Mkeystrokes/) and [Logan (1999)](https://www.unm.edu/~quadl/empiricaldata/Typographical-Errors-by-E.P.) show average typists produce errors at ~2-5% of keystrokes. The dominant error type (~40%) is adjacent-key substitution on the QWERTY layout. + +**API change:** Add `typo_chance: number` (0.0-0.10, default 0) to `TypeTextRequest`. Requires `smooth=true`. Typical human range is 0.02-0.05. + +**Typo position selection -- geometric gap, O(typos) not O(chars):** + +Instead of per-character Bernoulli trials (expensive), compute gaps between typo positions directly. For a 3% rate the average gap is ~33 characters. One `rng.Intn` per typo: + +```go +avgGap := int(1.0 / typoRate) // e.g., 33 for 3% +pos := avgGap/2 + rng.Intn(avgGap) // first typo position +for pos < len(runes) { + typoPositions = append(typoPositions, pos) + pos += avgGap/2 + rng.Intn(avgGap) // next gap: [avgGap/2, avgGap*3/2) +} +``` + +For a 200-char text at 3%: ~6 typos = ~6 `rng.Intn` calls for positions + ~6 for type selection = **12 total random calls** vs 200 with per-character Bernoulli. No `math.Log` or transcendental functions. + +**Typo type selection (one `rng.Intn(100)` per typo):** + +| Roll | Type | Weight | Mechanism | +|---|---|---|---| +| 0-59 | Adjacent key | 60% | Look up QWERTY neighbor from static `[26][]byte` array, O(1) | +| 60-79 | Doubling | 20% | Type the character twice | +| 80-94 | Transposition | 15% | Swap current and next character | +| 95-99 | Extra character | 5% | Insert a random adjacent key before the correct one | + +**Correction sequence:** At a typo position, the chunk is split and the correction is injected: + +``` +xdotool type --delay 80 -- "hel" # type up to typo point +xdotool type --delay 80 -- "p" # type wrong char (adjacent to 'l') + [Go sleep 350ms] # "oh no" realization pause +xdotool key BackSpace # delete wrong char + [Go sleep 80ms] # brief recovery +xdotool type --delay 80 -- "lo " # continue with correct text +``` + +**Correction timing:** +- Realization pause before backspace: `UniformJitter(rng, 350, 150, 150)` -> [200, 500]ms +- Backspace is rapid: no extra delay +- Recovery pause after correction: `UniformJitter(rng, 80, 30, 40)` -> [50, 110]ms + +**QWERTY adjacency data:** Static array, ~26 entries, initialized at package level. No maps, no heap allocation: + +```go +var qwertyAdj = [26][]byte{ + {'q', 'w', 's', 'z'}, // a + {'v', 'g', 'h', 'n'}, // b + {'x', 'd', 'f', 'v'}, // c + // ... etc +} +``` + +**Extra xdotool calls per typo:** ~3-4 (type wrong, backspace, type correct). At 3% rate on a 50-char sentence, that's ~1-2 typos adding ~4-8 extra calls. The realization pauses (200-500ms) dominate, so fork+exec overhead is negligible. + +**Cost summary:** O(typos) random calls + O(typos) extra xdotool calls. No per-character work. For typical text, typos add 0-3 extra correction sequences. + --- ## 3. Press Key -- Dwell via Inline Sleep @@ -199,7 +262,7 @@ xdotool mousemove 500 300 click 5 sleep 0.055 click 5 sleep 0.030 click 5 sleep | ------------- | ---------------- | --------------------------------- | ------------------------------------ | | `move_mouse` | O(points) (done) | O(points) Bezier + Box-Muller | Bezier curve + easeOutQuad + jitter | | `click_mouse` | 1 (same) | 1-2x `rand.Intn` | Uniform random dwell | -| `type_text` | O(words) | O(words) `rand.Intn` | Per-word type + Go-side sleep | +| `type_text` | O(words+typos) | O(words+typos) `rand.Intn` | Per-word type + Go-side sleep + optional typo injection | | `press_key` | 1 (same) | 1x `rand.Intn` | Inline keydown/sleep/keyup | | `scroll` | 1 (same) | O(ticks) smoothstep (3 muls each) | Eased inter-tick sleep | | `drag_mouse` | 1-3 (same) | O(points) Bezier (existing) | Bezier path + smoothstep step delays | From 5abb339e8e9b0d22f60521aceb213737c51972af Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Fri, 3 Apr 2026 11:06:16 -0600 Subject: [PATCH 4/5] feat: add human-like smooth typing with optional typo injection Adds smooth typing mode to POST /computer/type that types text in word-sized chunks with variable intra-word delays and natural inter-word pauses via xdotool, following the same Go-side sleep pattern as doMoveMouseSmooth. Optionally injects realistic typos (adjacent-key, doubling, transpose) using geometric gap sampling (O(typos) random calls, not O(chars)) with QWERTY adjacency lookup, then corrects them with backspace after a "realization" pause. New API fields on TypeTextRequest: - smooth: boolean (default false) - enable human-like timing - typo_chance: number 0.0-0.10 (default 0) - per-char typo rate New package: server/lib/typinghumanizer with word chunking, QWERTY adjacency map, and typo position generation. Made-with: Cursor --- server/cmd/api/api/computer.go | 179 ++++- server/go.sum | 5 +- server/lib/oapi/oapi.go | 630 +++++++----------- server/lib/typinghumanizer/typinghumanizer.go | 167 +++++ .../typinghumanizer/typinghumanizer_test.go | 192 ++++++ server/openapi.yaml | 19 +- 6 files changed, 780 insertions(+), 412 deletions(-) create mode 100644 server/lib/typinghumanizer/typinghumanizer.go create mode 100644 server/lib/typinghumanizer/typinghumanizer_test.go diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 2367cd68..b5f00dde 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -18,6 +18,7 @@ import ( "github.com/onkernel/kernel-images/server/lib/logger" "github.com/onkernel/kernel-images/server/lib/mousetrajectory" + "github.com/onkernel/kernel-images/server/lib/typinghumanizer" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -450,19 +451,20 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen } func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) error { + if body.Smooth != nil && *body.Smooth { + return s.doTypeTextSmooth(ctx, body) + } + log := logger.FromContext(ctx) - // Validate delay if provided if body.Delay != nil && *body.Delay < 0 { return &validationError{msg: "delay must be >= 0 milliseconds"} } - // Build xdotool arguments args := []string{"type"} if body.Delay != nil { args = append(args, "--delay", strconv.Itoa(*body.Delay)) } - // Use "--" to terminate options and pass raw text args = append(args, "--", body.Text) output, err := defaultXdoTool.Run(ctx, args...) @@ -474,6 +476,177 @@ func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) return nil } +func (s *ApiService) doTypeTextSmooth(ctx context.Context, body oapi.TypeTextRequest) error { + log := logger.FromContext(ctx) + + if body.TypoChance != nil && (*body.TypoChance < 0 || *body.TypoChance > 0.10) { + return &validationError{msg: "typo_chance must be between 0.0 and 0.10"} + } + + rng := rand.New(rand.NewSource(rand.Int63())) + runes := []rune(body.Text) + + var typoRate float64 + if body.TypoChance != nil { + typoRate = float64(*body.TypoChance) + } + typos := typinghumanizer.GenerateTypoPositions(rng, len(runes), typoRate) + + // Build a typo lookup set for O(1) access during chunk iteration + typoByPos := map[int]typinghumanizer.Typo{} + for _, typo := range typos { + typoByPos[typo.Pos] = typo + } + + chunks := typinghumanizer.SplitWordChunks(body.Text) + if len(chunks) == 0 { + return nil + } + + globalPos := 0 + for chunkIdx, chunk := range chunks { + select { + case <-ctx.Done(): + return &executionError{msg: "typing cancelled"} + default: + } + + chunkRunes := []rune(chunk) + chunkStart := globalPos + chunkEnd := chunkStart + len(chunkRunes) + + // Find typos within this chunk + var chunkTypo *typinghumanizer.Typo + for pos := chunkStart; pos < chunkEnd; pos++ { + if t, ok := typoByPos[pos]; ok { + chunkTypo = &t + break + } + } + + intraDelayMs := rng.Intn(70) + 50 + + if chunkTypo == nil { + if err := s.xdotoolTypeChunk(ctx, chunk, intraDelayMs); err != nil { + log.Error("xdotool type chunk failed", "err", err, "chunk", chunkIdx) + return &executionError{msg: "failed during smooth typing"} + } + } else { + localPos := chunkTypo.Pos - chunkStart + if err := s.typeChunkWithTypo(ctx, log, rng, chunkRunes, localPos, *chunkTypo, intraDelayMs); err != nil { + return err + } + } + + globalPos = chunkEnd + + if chunkIdx < len(chunks)-1 { + pause := typinghumanizer.UniformJitter(rng, 140, 60, 60) + if typinghumanizer.IsSentenceEnd(chunk) { + pause = pause * 3 / 2 + } + if err := sleepWithContext(ctx, pause); err != nil { + return &executionError{msg: "typing cancelled"} + } + } + } + + log.Info("executed smooth typing", "chunks", len(chunks), "typos", len(typos), "textLen", len(body.Text)) + return nil +} + +func (s *ApiService) xdotoolTypeChunk(ctx context.Context, text string, delayMs int) error { + args := []string{"type", "--delay", strconv.Itoa(delayMs), "--", text} + output, err := defaultXdoTool.Run(ctx, args...) + if err != nil { + return fmt.Errorf("xdotool type failed: %s (output: %s)", err, string(output)) + } + return nil +} + +func (s *ApiService) typeChunkWithTypo( + ctx context.Context, + log *slog.Logger, + rng *rand.Rand, + chunkRunes []rune, + typoLocalPos int, + typo typinghumanizer.Typo, + delayMs int, +) error { + // Type text before the typo + if typoLocalPos > 0 { + before := string(chunkRunes[:typoLocalPos]) + if err := s.xdotoolTypeChunk(ctx, before, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + } + + correctChar := chunkRunes[typoLocalPos] + var wrongText string + var backspaces int + + switch typo.Kind { + case typinghumanizer.TypoAdjacentKey: + wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) + backspaces = 1 + case typinghumanizer.TypoDoubling: + wrongText = string([]rune{correctChar, correctChar}) + backspaces = 2 + case typinghumanizer.TypoTranspose: + if typoLocalPos+1 < len(chunkRunes) { + wrongText = string([]rune{chunkRunes[typoLocalPos+1], correctChar}) + backspaces = 2 + } else { + wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) + backspaces = 1 + } + case typinghumanizer.TypoExtraChar: + wrongText = string([]rune{typinghumanizer.AdjacentKey(rng, correctChar), correctChar}) + backspaces = 2 + } + + // Type the wrong text + if err := s.xdotoolTypeChunk(ctx, wrongText, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + + // "Oh no" realization pause + realizationPause := typinghumanizer.UniformJitter(rng, 350, 150, 150) + if err := sleepWithContext(ctx, realizationPause); err != nil { + return &executionError{msg: "typing cancelled"} + } + + // Backspace to correct + bsArgs := make([]string, 0, backspaces*2) + for i := 0; i < backspaces; i++ { + bsArgs = append(bsArgs, "key", "BackSpace") + } + if output, err := defaultXdoTool.Run(ctx, bsArgs...); err != nil { + log.Error("xdotool backspace failed", "err", err, "output", string(output)) + return &executionError{msg: "failed during typo correction"} + } + + // Brief recovery pause + recoveryPause := typinghumanizer.UniformJitter(rng, 80, 30, 40) + if err := sleepWithContext(ctx, recoveryPause); err != nil { + return &executionError{msg: "typing cancelled"} + } + + // Type the correct remainder of the chunk from the typo position onward + var correctText string + if typo.Kind == typinghumanizer.TypoTranspose && typoLocalPos+1 < len(chunkRunes) { + correctText = string(chunkRunes[typoLocalPos:]) + } else { + correctText = string(chunkRunes[typoLocalPos:]) + } + + if err := s.xdotoolTypeChunk(ctx, correctText, delayMs); err != nil { + return &executionError{msg: "failed during smooth typing"} + } + + return nil +} + func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestObject) (oapi.TypeTextResponseObject, error) { s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/go.sum b/server/go.sum index baa1fd2c..d13d59b4 100644 --- a/server/go.sum +++ b/server/go.sum @@ -127,8 +127,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nrednav/cuid2 v1.1.0 h1:Y2P9Fo1Iz7lKuwcn+fS0mbxkNvEqoNLUtm0+moHCnYc= github.com/nrednav/cuid2 v1.1.0/go.mod h1:jBjkJAI+QLM4EUGvtwGDHC1cP1QQrRNfLo/A7qJFDhA= -github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= -github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -175,9 +173,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4854c577..127d1ea5 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1,6 +1,6 @@ // Package oapi provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package oapi import ( @@ -9,7 +9,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "mime/multipart" @@ -35,24 +34,6 @@ const ( ClickMouseRequestButtonRight ClickMouseRequestButton = "right" ) -// Valid indicates whether the value is a known member of the ClickMouseRequestButton enum. -func (e ClickMouseRequestButton) Valid() bool { - switch e { - case ClickMouseRequestButtonBack: - return true - case ClickMouseRequestButtonForward: - return true - case ClickMouseRequestButtonLeft: - return true - case ClickMouseRequestButtonMiddle: - return true - case ClickMouseRequestButtonRight: - return true - default: - return false - } -} - // Defines values for ClickMouseRequestClickType. const ( Click ClickMouseRequestClickType = "click" @@ -60,20 +41,6 @@ const ( Up ClickMouseRequestClickType = "up" ) -// Valid indicates whether the value is a known member of the ClickMouseRequestClickType enum. -func (e ClickMouseRequestClickType) Valid() bool { - switch e { - case Click: - return true - case Down: - return true - case Up: - return true - default: - return false - } -} - // Defines values for ComputerActionType. const ( ClickMouse ComputerActionType = "click_mouse" @@ -86,30 +53,6 @@ const ( TypeText ComputerActionType = "type_text" ) -// Valid indicates whether the value is a known member of the ComputerActionType enum. -func (e ComputerActionType) Valid() bool { - switch e { - case ClickMouse: - return true - case DragMouse: - return true - case MoveMouse: - return true - case PressKey: - return true - case Scroll: - return true - case SetCursor: - return true - case Sleep: - return true - case TypeText: - return true - default: - return false - } -} - // Defines values for DragMouseRequestButton. const ( DragMouseRequestButtonLeft DragMouseRequestButton = "left" @@ -117,20 +60,6 @@ const ( DragMouseRequestButtonRight DragMouseRequestButton = "right" ) -// Valid indicates whether the value is a known member of the DragMouseRequestButton enum. -func (e DragMouseRequestButton) Valid() bool { - switch e { - case DragMouseRequestButtonLeft: - return true - case DragMouseRequestButtonMiddle: - return true - case DragMouseRequestButtonRight: - return true - default: - return false - } -} - // Defines values for FileSystemEventType. const ( CREATE FileSystemEventType = "CREATE" @@ -139,22 +68,6 @@ const ( WRITE FileSystemEventType = "WRITE" ) -// Valid indicates whether the value is a known member of the FileSystemEventType enum. -func (e FileSystemEventType) Valid() bool { - switch e { - case CREATE: - return true - case DELETE: - return true - case RENAME: - return true - case WRITE: - return true - default: - return false - } -} - // Defines values for PatchDisplayRequestRefreshRate. const ( N10 PatchDisplayRequestRefreshRate = 10 @@ -163,22 +76,6 @@ const ( N60 PatchDisplayRequestRefreshRate = 60 ) -// Valid indicates whether the value is a known member of the PatchDisplayRequestRefreshRate enum. -func (e PatchDisplayRequestRefreshRate) Valid() bool { - switch e { - case N10: - return true - case N25: - return true - case N30: - return true - case N60: - return true - default: - return false - } -} - // Defines values for ProcessKillRequestSignal. const ( HUP ProcessKillRequestSignal = "HUP" @@ -187,73 +84,23 @@ const ( TERM ProcessKillRequestSignal = "TERM" ) -// Valid indicates whether the value is a known member of the ProcessKillRequestSignal enum. -func (e ProcessKillRequestSignal) Valid() bool { - switch e { - case HUP: - return true - case INT: - return true - case KILL: - return true - case TERM: - return true - default: - return false - } -} - // Defines values for ProcessStatusState. const ( Exited ProcessStatusState = "exited" Running ProcessStatusState = "running" ) -// Valid indicates whether the value is a known member of the ProcessStatusState enum. -func (e ProcessStatusState) Valid() bool { - switch e { - case Exited: - return true - case Running: - return true - default: - return false - } -} - // Defines values for ProcessStreamEventEvent. const ( Exit ProcessStreamEventEvent = "exit" ) -// Valid indicates whether the value is a known member of the ProcessStreamEventEvent enum. -func (e ProcessStreamEventEvent) Valid() bool { - switch e { - case Exit: - return true - default: - return false - } -} - // Defines values for ProcessStreamEventStream. const ( Stderr ProcessStreamEventStream = "stderr" Stdout ProcessStreamEventStream = "stdout" ) -// Valid indicates whether the value is a known member of the ProcessStreamEventStream enum. -func (e ProcessStreamEventStream) Valid() bool { - switch e { - case Stderr: - return true - case Stdout: - return true - default: - return false - } -} - // Defines values for DownloadDirZstdParamsCompressionLevel. const ( Best DownloadDirZstdParamsCompressionLevel = "best" @@ -262,40 +109,12 @@ const ( Fastest DownloadDirZstdParamsCompressionLevel = "fastest" ) -// Valid indicates whether the value is a known member of the DownloadDirZstdParamsCompressionLevel enum. -func (e DownloadDirZstdParamsCompressionLevel) Valid() bool { - switch e { - case Best: - return true - case Better: - return true - case Default: - return true - case Fastest: - return true - default: - return false - } -} - // Defines values for LogsStreamParamsSource. const ( Path LogsStreamParamsSource = "path" Supervisor LogsStreamParamsSource = "supervisor" ) -// Valid indicates whether the value is a known member of the LogsStreamParamsSource enum. -func (e LogsStreamParamsSource) Valid() bool { - switch e { - case Path: - return true - case Supervisor: - return true - default: - return false - } -} - // 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. @@ -590,19 +409,19 @@ type ProcessExecRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user,omitempty"` + AsUser *string `json:"as_user"` // Command Executable or shell command to run. Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd,omitempty"` + Cwd *string `json:"cwd"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec,omitempty"` + TimeoutSec *int `json:"timeout_sec"` } // ProcessExecResult Result of a synchronous command execution. @@ -650,7 +469,7 @@ type ProcessSpawnRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user,omitempty"` + AsUser *string `json:"as_user"` // Cols Initial terminal columns when allocate_tty is true. Cols *int `json:"cols,omitempty"` @@ -659,7 +478,7 @@ type ProcessSpawnRequest struct { Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd,omitempty"` + Cwd *string `json:"cwd"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` @@ -668,7 +487,7 @@ type ProcessSpawnRequest struct { Rows *int `json:"rows,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec,omitempty"` + TimeoutSec *int `json:"timeout_sec"` } // ProcessSpawnResult Information about a spawned process. @@ -689,7 +508,7 @@ type ProcessStatus struct { CpuPct *float32 `json:"cpu_pct,omitempty"` // ExitCode Exit code if the process has exited. - ExitCode *int `json:"exit_code,omitempty"` + ExitCode *int `json:"exit_code"` // MemBytes Estimated resident memory usage in bytes. MemBytes *int `json:"mem_bytes,omitempty"` @@ -737,12 +556,12 @@ type ProcessStreamEventStream string // RecorderInfo defines model for RecorderInfo. type RecorderInfo struct { // FinishedAt Timestamp when recording finished - FinishedAt *time.Time `json:"finished_at,omitempty"` + FinishedAt *time.Time `json:"finished_at"` Id string `json:"id"` IsRecording bool `json:"isRecording"` // StartedAt Timestamp when recording started - StartedAt *time.Time `json:"started_at,omitempty"` + StartedAt *time.Time `json:"started_at"` } // ScreenshotRegion defines model for ScreenshotRegion. @@ -845,11 +664,22 @@ type StopRecordingRequest struct { // TypeTextRequest defines model for TypeTextRequest. type TypeTextRequest struct { - // Delay Delay in milliseconds between keystrokes + // Delay Delay in milliseconds between keystrokes. Ignored when smooth is true. Delay *int `json:"delay,omitempty"` + // Smooth Use human-like variable keystroke timing instead of a fixed delay. + // When enabled, text is typed in word-sized chunks with variable + // intra-word delays and natural inter-word pauses. The delay field + // is ignored when smooth is true. + Smooth *bool `json:"smooth,omitempty"` + // Text Text to type on the host computer Text string `json:"text"` + + // TypoChance Probability (0.0-0.10) of a typo per character, which is then + // corrected with backspace. Requires smooth to be true. Set to 0 + // to disable. Typical human range is 0.02-0.05. + TypoChance *float32 `json:"typo_chance,omitempty"` } // WriteClipboardRequest defines model for WriteClipboardRequest. @@ -3001,7 +2831,7 @@ func NewDownloadDirZipRequest(server string, params *DownloadDirZipParams) (*htt if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3046,7 +2876,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3060,7 +2890,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params.CompressionLevel != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "compression_level", *params.CompressionLevel, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "compression_level", runtime.ParamLocationQuery, *params.CompressionLevel); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3107,7 +2937,7 @@ func NewFileInfoRequest(server string, params *FileInfoParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3152,7 +2982,7 @@ func NewListFilesRequest(server string, params *ListFilesParams) (*http.Request, if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3237,7 +3067,7 @@ func NewReadFileRequest(server string, params *ReadFileParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3433,7 +3263,7 @@ func NewStopFsWatchRequest(server string, watchId string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) if err != nil { return nil, err } @@ -3467,7 +3297,7 @@ func NewStreamFsEventsRequest(server string, watchId string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) if err != nil { return nil, err } @@ -3517,7 +3347,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3531,7 +3361,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params.Mode != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "mode", *params.Mode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "mode", runtime.ParamLocationQuery, *params.Mode); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3580,7 +3410,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "source", params.Source, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3594,7 +3424,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Follow != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "follow", *params.Follow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3610,7 +3440,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Path != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", *params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3626,7 +3456,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.SupervisorProcess != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "supervisor_process", *params.SupervisorProcess, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3788,7 +3618,7 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3835,7 +3665,7 @@ func NewProcessResizeRequestWithBody(server string, processId openapi_types.UUID var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3871,7 +3701,7 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3916,7 +3746,7 @@ func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -3952,7 +3782,7 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) var pathParam0 string - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) if err != nil { return nil, err } @@ -4044,7 +3874,7 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) if params.Id != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "id", *params.Id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -8710,7 +8540,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZip(w http.ResponseWriter, r *http return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8744,7 +8574,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8752,7 +8582,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt // ------------- Optional query parameter "compression_level" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "compression_level", Err: err}) return @@ -8786,7 +8616,7 @@ func (siw *ServerInterfaceWrapper) FileInfo(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8820,7 +8650,7 @@ func (siw *ServerInterfaceWrapper) ListFiles(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8868,7 +8698,7 @@ func (siw *ServerInterfaceWrapper) ReadFile(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8963,7 +8793,7 @@ func (siw *ServerInterfaceWrapper) StopFsWatch(w http.ResponseWriter, r *http.Re // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -8988,7 +8818,7 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -9022,7 +8852,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -9030,7 +8860,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ // ------------- Optional query parameter "mode" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "mode", r.URL.Query(), ¶ms.Mode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err}) return @@ -9064,7 +8894,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req return } - err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, true, "source", r.URL.Query(), ¶ms.Source) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) return @@ -9072,7 +8902,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "follow" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "follow", r.URL.Query(), ¶ms.Follow, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err}) return @@ -9080,7 +8910,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "path" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "path", r.URL.Query(), ¶ms.Path) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -9088,7 +8918,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "supervisor_process" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "supervisor_process", Err: err}) return @@ -9155,7 +8985,7 @@ func (siw *ServerInterfaceWrapper) ProcessKill(w http.ResponseWriter, r *http.Re // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9180,7 +9010,7 @@ func (siw *ServerInterfaceWrapper) ProcessResize(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9205,7 +9035,7 @@ func (siw *ServerInterfaceWrapper) ProcessStatus(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9230,7 +9060,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdin(w http.ResponseWriter, r *http.R // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9255,7 +9085,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdoutStream(w http.ResponseWriter, r // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9296,7 +9126,7 @@ func (siw *ServerInterfaceWrapper) DownloadRecording(w http.ResponseWriter, r *h // ------------- Optional query parameter "id" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "id", r.URL.Query(), ¶ms.Id, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameter("form", true, false, "id", r.URL.Query(), ¶ms.Id) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) return @@ -12159,13 +11989,10 @@ func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request) var body TakeScreenshotJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + 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.TakeScreenshot(ctx, request.(TakeScreenshotRequestObject)) @@ -13045,13 +12872,10 @@ func (sh *strictHandler) DeleteRecording(w http.ResponseWriter, r *http.Request) var body DeleteRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + 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.DeleteRecording(ctx, request.(DeleteRecordingRequestObject)) @@ -13129,13 +12953,10 @@ func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request) var body StartRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + 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.StartRecording(ctx, request.(StartRecordingRequestObject)) @@ -13163,13 +12984,10 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { var body StopRecordingJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - if !errors.Is(err, io.EOF) { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - } else { - request.Body = &body + 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.StopRecording(ctx, request.(StopRecordingRequestObject)) @@ -13194,154 +13012,158 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/typinghumanizer/typinghumanizer.go b/server/lib/typinghumanizer/typinghumanizer.go new file mode 100644 index 00000000..66b00790 --- /dev/null +++ b/server/lib/typinghumanizer/typinghumanizer.go @@ -0,0 +1,167 @@ +package typinghumanizer + +import ( + "math/rand" + "strings" + "time" + "unicode" +) + +// UniformJitter returns a random duration in [baseMs-jitterMs, baseMs+jitterMs], +// clamped to a minimum of minMs. +func UniformJitter(rng *rand.Rand, baseMs, jitterMs, minMs int) time.Duration { + ms := baseMs - jitterMs + rng.Intn(2*jitterMs+1) + if ms < minMs { + ms = minMs + } + return time.Duration(ms) * time.Millisecond +} + +// SplitWordChunks splits text into word-sized chunks, keeping trailing +// whitespace and punctuation attached to the preceding word. +// For example, "Hello world. How are you?" becomes: +// +// ["Hello ", "world. ", "How ", "are ", "you?"] +func SplitWordChunks(text string) []string { + if len(text) == 0 { + return nil + } + + var chunks []string + var current strings.Builder + + runes := []rune(text) + i := 0 + for i < len(runes) { + r := runes[i] + current.WriteRune(r) + i++ + + if unicode.IsSpace(r) { + for i < len(runes) && unicode.IsSpace(runes[i]) { + current.WriteRune(runes[i]) + i++ + } + chunks = append(chunks, current.String()) + current.Reset() + } + } + + if current.Len() > 0 { + chunks = append(chunks, current.String()) + } + + return chunks +} + +// IsSentenceEnd returns true if the chunk ends with sentence-ending punctuation +// (before any trailing whitespace). +func IsSentenceEnd(chunk string) bool { + trimmed := strings.TrimRightFunc(chunk, unicode.IsSpace) + if len(trimmed) == 0 { + return false + } + last := trimmed[len(trimmed)-1] + return last == '.' || last == '!' || last == '?' +} + +// TypoKind identifies the type of typo to inject. +type TypoKind int + +const ( + TypoAdjacentKey TypoKind = iota // Hit a neighboring key + TypoDoubling // Type the character twice + TypoTranspose // Swap current and next character + TypoExtraChar // Insert a random adjacent key before the correct one +) + +// Typo describes a single typo at a position in the text. +type Typo struct { + Pos int // Character index in the rune slice + Kind TypoKind // What kind of typo +} + +// qwertyAdj maps each lowercase letter to its adjacent keys on a QWERTY layout. +var qwertyAdj = [26][]byte{ + {'q', 'w', 's', 'z'}, // a + {'v', 'g', 'h', 'n'}, // b + {'x', 'd', 'f', 'v'}, // c + {'s', 'e', 'r', 'f', 'c', 'x'}, // d + {'w', 's', 'd', 'r'}, // e + {'d', 'r', 't', 'g', 'v', 'c'}, // f + {'f', 't', 'y', 'h', 'b', 'v'}, // g + {'g', 'y', 'u', 'j', 'n', 'b'}, // h + {'u', 'j', 'k', 'o'}, // i + {'h', 'u', 'i', 'k', 'n', 'm'}, // j + {'j', 'i', 'o', 'l', 'm'}, // k + {'k', 'o', 'p'}, // l + {'n', 'j', 'k'}, // m + {'b', 'h', 'j', 'm'}, // n + {'i', 'k', 'l', 'p'}, // o + {'o', 'l'}, // p + {'w', 'a'}, // q + {'e', 'd', 'f', 't'}, // r + {'a', 'w', 'e', 'd', 'x', 'z'}, // s + {'r', 'f', 'g', 'y'}, // t + {'y', 'h', 'j', 'i'}, // u + {'c', 'f', 'g', 'b'}, // v + {'q', 'a', 's', 'e'}, // w + {'z', 's', 'd', 'c'}, // x + {'t', 'g', 'h', 'u'}, // y + {'a', 's', 'x'}, // z +} + +// AdjacentKey returns a random QWERTY neighbor of the given character. +// If the character has no known neighbors (non-letter), it returns the +// character itself unchanged. +func AdjacentKey(rng *rand.Rand, ch rune) rune { + lower := unicode.ToLower(ch) + if lower < 'a' || lower > 'z' { + return ch + } + neighbors := qwertyAdj[lower-'a'] + if len(neighbors) == 0 { + return ch + } + adj := rune(neighbors[rng.Intn(len(neighbors))]) + if unicode.IsUpper(ch) { + adj = unicode.ToUpper(adj) + } + return adj +} + +// GenerateTypoPositions computes typo positions using geometric gap sampling. +// O(typos) random calls, not O(chars). Returns a sorted slice of Typo structs. +func GenerateTypoPositions(rng *rand.Rand, textLen int, typoRate float64) []Typo { + if typoRate <= 0 || textLen <= 1 { + return nil + } + avgGap := int(1.0 / typoRate) + if avgGap < 2 { + avgGap = 2 + } + + var typos []Typo + halfGap := avgGap / 2 + if halfGap < 1 { + halfGap = 1 + } + pos := halfGap + rng.Intn(avgGap) + for pos < textLen { + roll := rng.Intn(100) + var kind TypoKind + switch { + case roll < 60: + kind = TypoAdjacentKey + case roll < 80: + kind = TypoDoubling + case roll < 95: + kind = TypoTranspose + default: + kind = TypoExtraChar + } + typos = append(typos, Typo{Pos: pos, Kind: kind}) + pos += halfGap + rng.Intn(avgGap) + } + return typos +} diff --git a/server/lib/typinghumanizer/typinghumanizer_test.go b/server/lib/typinghumanizer/typinghumanizer_test.go new file mode 100644 index 00000000..5271aa80 --- /dev/null +++ b/server/lib/typinghumanizer/typinghumanizer_test.go @@ -0,0 +1,192 @@ +package typinghumanizer + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUniformJitter(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("stays within range", func(t *testing.T) { + for i := 0; i < 1000; i++ { + d := UniformJitter(rng, 100, 30, 50) + ms := d.Milliseconds() + assert.GreaterOrEqual(t, ms, int64(50), "should be >= minMs") + assert.LessOrEqual(t, ms, int64(130), "should be <= baseMs+jitterMs") + } + }) + + t.Run("clamps to minimum", func(t *testing.T) { + for i := 0; i < 100; i++ { + d := UniformJitter(rng, 10, 20, 5) + assert.GreaterOrEqual(t, d.Milliseconds(), int64(5)) + } + }) + + t.Run("zero jitter returns base", func(t *testing.T) { + d := UniformJitter(rng, 100, 0, 0) + assert.Equal(t, 100*time.Millisecond, d) + }) +} + +func TestSplitWordChunks(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "simple sentence", + input: "Hello world", + expected: []string{"Hello ", "world"}, + }, + { + name: "with punctuation", + input: "Hello world. How are you?", + expected: []string{"Hello ", "world. ", "How ", "are ", "you?"}, + }, + { + name: "single word", + input: "Hello", + expected: []string{"Hello"}, + }, + { + name: "empty string", + input: "", + expected: nil, + }, + { + name: "only spaces", + input: " ", + expected: []string{" "}, + }, + { + name: "multiple spaces between words", + input: "Hello world", + expected: []string{"Hello ", "world"}, + }, + { + name: "trailing space", + input: "Hello ", + expected: []string{"Hello "}, + }, + { + name: "leading space", + input: " Hello", + expected: []string{" ", "Hello"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SplitWordChunks(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestIsSentenceEnd(t *testing.T) { + tests := []struct { + chunk string + expected bool + }{ + {"world. ", true}, + {"you?", true}, + {"wow! ", true}, + {"Hello ", false}, + {"word", false}, + {"", false}, + {" ", false}, + {"end.", true}, + } + + for _, tt := range tests { + t.Run(tt.chunk, func(t *testing.T) { + assert.Equal(t, tt.expected, IsSentenceEnd(tt.chunk)) + }) + } +} + +func TestAdjacentKey(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("returns a neighbor for lowercase letters", func(t *testing.T) { + for i := 0; i < 100; i++ { + adj := AdjacentKey(rng, 'a') + assert.Contains(t, []rune{'q', 'w', 's', 'z'}, adj) + } + }) + + t.Run("preserves uppercase", func(t *testing.T) { + for i := 0; i < 100; i++ { + adj := AdjacentKey(rng, 'A') + assert.Contains(t, []rune{'Q', 'W', 'S', 'Z'}, adj) + } + }) + + t.Run("returns same char for non-letters", func(t *testing.T) { + assert.Equal(t, '5', AdjacentKey(rng, '5')) + assert.Equal(t, '.', AdjacentKey(rng, '.')) + assert.Equal(t, ' ', AdjacentKey(rng, ' ')) + }) +} + +func TestGenerateTypoPositions(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + t.Run("zero rate returns nil", func(t *testing.T) { + assert.Nil(t, GenerateTypoPositions(rng, 100, 0)) + }) + + t.Run("short text returns nil or few typos", func(t *testing.T) { + typos := GenerateTypoPositions(rng, 1, 0.05) + assert.Nil(t, typos) + }) + + t.Run("positions are within bounds and sorted", func(t *testing.T) { + textLen := 200 + typos := GenerateTypoPositions(rng, textLen, 0.03) + for i, typo := range typos { + assert.GreaterOrEqual(t, typo.Pos, 0) + assert.Less(t, typo.Pos, textLen) + if i > 0 { + assert.Greater(t, typo.Pos, typos[i-1].Pos, "positions must be strictly increasing") + } + } + }) + + t.Run("roughly matches expected count", func(t *testing.T) { + textLen := 1000 + rate := 0.03 + totalTypos := 0 + runs := 200 + for i := 0; i < runs; i++ { + localRng := rand.New(rand.NewSource(int64(i))) + typos := GenerateTypoPositions(localRng, textLen, rate) + totalTypos += len(typos) + } + avgTypos := float64(totalTypos) / float64(runs) + expected := float64(textLen) * rate + assert.InDelta(t, expected, avgTypos, expected*0.3, "average typo count should be near expected") + }) + + t.Run("kind distribution is weighted", func(t *testing.T) { + counts := map[TypoKind]int{} + for i := 0; i < 500; i++ { + localRng := rand.New(rand.NewSource(int64(i))) + typos := GenerateTypoPositions(localRng, 500, 0.05) + for _, typo := range typos { + counts[typo.Kind]++ + } + } + total := counts[TypoAdjacentKey] + counts[TypoDoubling] + counts[TypoTranspose] + counts[TypoExtraChar] + require.Greater(t, total, 0) + adjPct := float64(counts[TypoAdjacentKey]) / float64(total) + assert.InDelta(t, 0.60, adjPct, 0.10, "adjacent key should be ~60%%") + }) +} diff --git a/server/openapi.yaml b/server/openapi.yaml index 79396d80..737e1db6 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1367,9 +1367,26 @@ components: description: Text to type on the host computer delay: type: integer - description: Delay in milliseconds between keystrokes + description: Delay in milliseconds between keystrokes. Ignored when smooth is true. minimum: 0 default: 0 + smooth: + type: boolean + description: | + Use human-like variable keystroke timing instead of a fixed delay. + When enabled, text is typed in word-sized chunks with variable + intra-word delays and natural inter-word pauses. The delay field + is ignored when smooth is true. + default: false + typo_chance: + type: number + description: | + Probability (0.0-0.10) of a typo per character, which is then + corrected with backspace. Requires smooth to be true. Set to 0 + to disable. Typical human range is 0.02-0.05. + minimum: 0 + maximum: 0.10 + default: 0 additionalProperties: false ClipboardContent: type: object From b8fb87c446743a3702c61cf82cbdb0cefdaf7211 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Fri, 3 Apr 2026 11:08:43 -0600 Subject: [PATCH 5/5] Revert "feat: add human-like smooth typing with optional typo injection" This reverts commit 5abb339e8e9b0d22f60521aceb213737c51972af. --- server/cmd/api/api/computer.go | 179 +---- server/go.sum | 5 +- server/lib/oapi/oapi.go | 630 +++++++++++------- server/lib/typinghumanizer/typinghumanizer.go | 167 ----- .../typinghumanizer/typinghumanizer_test.go | 192 ------ server/openapi.yaml | 19 +- 6 files changed, 412 insertions(+), 780 deletions(-) delete mode 100644 server/lib/typinghumanizer/typinghumanizer.go delete mode 100644 server/lib/typinghumanizer/typinghumanizer_test.go diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index b5f00dde..2367cd68 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -18,7 +18,6 @@ import ( "github.com/onkernel/kernel-images/server/lib/logger" "github.com/onkernel/kernel-images/server/lib/mousetrajectory" - "github.com/onkernel/kernel-images/server/lib/typinghumanizer" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -451,20 +450,19 @@ func (s *ApiService) TakeScreenshot(ctx context.Context, request oapi.TakeScreen } func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) error { - if body.Smooth != nil && *body.Smooth { - return s.doTypeTextSmooth(ctx, body) - } - log := logger.FromContext(ctx) + // Validate delay if provided if body.Delay != nil && *body.Delay < 0 { return &validationError{msg: "delay must be >= 0 milliseconds"} } + // Build xdotool arguments args := []string{"type"} if body.Delay != nil { args = append(args, "--delay", strconv.Itoa(*body.Delay)) } + // Use "--" to terminate options and pass raw text args = append(args, "--", body.Text) output, err := defaultXdoTool.Run(ctx, args...) @@ -476,177 +474,6 @@ func (s *ApiService) doTypeText(ctx context.Context, body oapi.TypeTextRequest) return nil } -func (s *ApiService) doTypeTextSmooth(ctx context.Context, body oapi.TypeTextRequest) error { - log := logger.FromContext(ctx) - - if body.TypoChance != nil && (*body.TypoChance < 0 || *body.TypoChance > 0.10) { - return &validationError{msg: "typo_chance must be between 0.0 and 0.10"} - } - - rng := rand.New(rand.NewSource(rand.Int63())) - runes := []rune(body.Text) - - var typoRate float64 - if body.TypoChance != nil { - typoRate = float64(*body.TypoChance) - } - typos := typinghumanizer.GenerateTypoPositions(rng, len(runes), typoRate) - - // Build a typo lookup set for O(1) access during chunk iteration - typoByPos := map[int]typinghumanizer.Typo{} - for _, typo := range typos { - typoByPos[typo.Pos] = typo - } - - chunks := typinghumanizer.SplitWordChunks(body.Text) - if len(chunks) == 0 { - return nil - } - - globalPos := 0 - for chunkIdx, chunk := range chunks { - select { - case <-ctx.Done(): - return &executionError{msg: "typing cancelled"} - default: - } - - chunkRunes := []rune(chunk) - chunkStart := globalPos - chunkEnd := chunkStart + len(chunkRunes) - - // Find typos within this chunk - var chunkTypo *typinghumanizer.Typo - for pos := chunkStart; pos < chunkEnd; pos++ { - if t, ok := typoByPos[pos]; ok { - chunkTypo = &t - break - } - } - - intraDelayMs := rng.Intn(70) + 50 - - if chunkTypo == nil { - if err := s.xdotoolTypeChunk(ctx, chunk, intraDelayMs); err != nil { - log.Error("xdotool type chunk failed", "err", err, "chunk", chunkIdx) - return &executionError{msg: "failed during smooth typing"} - } - } else { - localPos := chunkTypo.Pos - chunkStart - if err := s.typeChunkWithTypo(ctx, log, rng, chunkRunes, localPos, *chunkTypo, intraDelayMs); err != nil { - return err - } - } - - globalPos = chunkEnd - - if chunkIdx < len(chunks)-1 { - pause := typinghumanizer.UniformJitter(rng, 140, 60, 60) - if typinghumanizer.IsSentenceEnd(chunk) { - pause = pause * 3 / 2 - } - if err := sleepWithContext(ctx, pause); err != nil { - return &executionError{msg: "typing cancelled"} - } - } - } - - log.Info("executed smooth typing", "chunks", len(chunks), "typos", len(typos), "textLen", len(body.Text)) - return nil -} - -func (s *ApiService) xdotoolTypeChunk(ctx context.Context, text string, delayMs int) error { - args := []string{"type", "--delay", strconv.Itoa(delayMs), "--", text} - output, err := defaultXdoTool.Run(ctx, args...) - if err != nil { - return fmt.Errorf("xdotool type failed: %s (output: %s)", err, string(output)) - } - return nil -} - -func (s *ApiService) typeChunkWithTypo( - ctx context.Context, - log *slog.Logger, - rng *rand.Rand, - chunkRunes []rune, - typoLocalPos int, - typo typinghumanizer.Typo, - delayMs int, -) error { - // Type text before the typo - if typoLocalPos > 0 { - before := string(chunkRunes[:typoLocalPos]) - if err := s.xdotoolTypeChunk(ctx, before, delayMs); err != nil { - return &executionError{msg: "failed during smooth typing"} - } - } - - correctChar := chunkRunes[typoLocalPos] - var wrongText string - var backspaces int - - switch typo.Kind { - case typinghumanizer.TypoAdjacentKey: - wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) - backspaces = 1 - case typinghumanizer.TypoDoubling: - wrongText = string([]rune{correctChar, correctChar}) - backspaces = 2 - case typinghumanizer.TypoTranspose: - if typoLocalPos+1 < len(chunkRunes) { - wrongText = string([]rune{chunkRunes[typoLocalPos+1], correctChar}) - backspaces = 2 - } else { - wrongText = string(typinghumanizer.AdjacentKey(rng, correctChar)) - backspaces = 1 - } - case typinghumanizer.TypoExtraChar: - wrongText = string([]rune{typinghumanizer.AdjacentKey(rng, correctChar), correctChar}) - backspaces = 2 - } - - // Type the wrong text - if err := s.xdotoolTypeChunk(ctx, wrongText, delayMs); err != nil { - return &executionError{msg: "failed during smooth typing"} - } - - // "Oh no" realization pause - realizationPause := typinghumanizer.UniformJitter(rng, 350, 150, 150) - if err := sleepWithContext(ctx, realizationPause); err != nil { - return &executionError{msg: "typing cancelled"} - } - - // Backspace to correct - bsArgs := make([]string, 0, backspaces*2) - for i := 0; i < backspaces; i++ { - bsArgs = append(bsArgs, "key", "BackSpace") - } - if output, err := defaultXdoTool.Run(ctx, bsArgs...); err != nil { - log.Error("xdotool backspace failed", "err", err, "output", string(output)) - return &executionError{msg: "failed during typo correction"} - } - - // Brief recovery pause - recoveryPause := typinghumanizer.UniformJitter(rng, 80, 30, 40) - if err := sleepWithContext(ctx, recoveryPause); err != nil { - return &executionError{msg: "typing cancelled"} - } - - // Type the correct remainder of the chunk from the typo position onward - var correctText string - if typo.Kind == typinghumanizer.TypoTranspose && typoLocalPos+1 < len(chunkRunes) { - correctText = string(chunkRunes[typoLocalPos:]) - } else { - correctText = string(chunkRunes[typoLocalPos:]) - } - - if err := s.xdotoolTypeChunk(ctx, correctText, delayMs); err != nil { - return &executionError{msg: "failed during smooth typing"} - } - - return nil -} - func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestObject) (oapi.TypeTextResponseObject, error) { s.inputMu.Lock() defer s.inputMu.Unlock() diff --git a/server/go.sum b/server/go.sum index d13d59b4..baa1fd2c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -127,6 +127,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nrednav/cuid2 v1.1.0 h1:Y2P9Fo1Iz7lKuwcn+fS0mbxkNvEqoNLUtm0+moHCnYc= github.com/nrednav/cuid2 v1.1.0/go.mod h1:jBjkJAI+QLM4EUGvtwGDHC1cP1QQrRNfLo/A7qJFDhA= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -173,8 +175,9 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 127d1ea5..4854c577 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1,6 +1,6 @@ // Package oapi provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. package oapi import ( @@ -9,6 +9,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "mime/multipart" @@ -34,6 +35,24 @@ const ( ClickMouseRequestButtonRight ClickMouseRequestButton = "right" ) +// Valid indicates whether the value is a known member of the ClickMouseRequestButton enum. +func (e ClickMouseRequestButton) Valid() bool { + switch e { + case ClickMouseRequestButtonBack: + return true + case ClickMouseRequestButtonForward: + return true + case ClickMouseRequestButtonLeft: + return true + case ClickMouseRequestButtonMiddle: + return true + case ClickMouseRequestButtonRight: + return true + default: + return false + } +} + // Defines values for ClickMouseRequestClickType. const ( Click ClickMouseRequestClickType = "click" @@ -41,6 +60,20 @@ const ( Up ClickMouseRequestClickType = "up" ) +// Valid indicates whether the value is a known member of the ClickMouseRequestClickType enum. +func (e ClickMouseRequestClickType) Valid() bool { + switch e { + case Click: + return true + case Down: + return true + case Up: + return true + default: + return false + } +} + // Defines values for ComputerActionType. const ( ClickMouse ComputerActionType = "click_mouse" @@ -53,6 +86,30 @@ const ( TypeText ComputerActionType = "type_text" ) +// Valid indicates whether the value is a known member of the ComputerActionType enum. +func (e ComputerActionType) Valid() bool { + switch e { + case ClickMouse: + return true + case DragMouse: + return true + case MoveMouse: + return true + case PressKey: + return true + case Scroll: + return true + case SetCursor: + return true + case Sleep: + return true + case TypeText: + return true + default: + return false + } +} + // Defines values for DragMouseRequestButton. const ( DragMouseRequestButtonLeft DragMouseRequestButton = "left" @@ -60,6 +117,20 @@ const ( DragMouseRequestButtonRight DragMouseRequestButton = "right" ) +// Valid indicates whether the value is a known member of the DragMouseRequestButton enum. +func (e DragMouseRequestButton) Valid() bool { + switch e { + case DragMouseRequestButtonLeft: + return true + case DragMouseRequestButtonMiddle: + return true + case DragMouseRequestButtonRight: + return true + default: + return false + } +} + // Defines values for FileSystemEventType. const ( CREATE FileSystemEventType = "CREATE" @@ -68,6 +139,22 @@ const ( WRITE FileSystemEventType = "WRITE" ) +// Valid indicates whether the value is a known member of the FileSystemEventType enum. +func (e FileSystemEventType) Valid() bool { + switch e { + case CREATE: + return true + case DELETE: + return true + case RENAME: + return true + case WRITE: + return true + default: + return false + } +} + // Defines values for PatchDisplayRequestRefreshRate. const ( N10 PatchDisplayRequestRefreshRate = 10 @@ -76,6 +163,22 @@ const ( N60 PatchDisplayRequestRefreshRate = 60 ) +// Valid indicates whether the value is a known member of the PatchDisplayRequestRefreshRate enum. +func (e PatchDisplayRequestRefreshRate) Valid() bool { + switch e { + case N10: + return true + case N25: + return true + case N30: + return true + case N60: + return true + default: + return false + } +} + // Defines values for ProcessKillRequestSignal. const ( HUP ProcessKillRequestSignal = "HUP" @@ -84,23 +187,73 @@ const ( TERM ProcessKillRequestSignal = "TERM" ) +// Valid indicates whether the value is a known member of the ProcessKillRequestSignal enum. +func (e ProcessKillRequestSignal) Valid() bool { + switch e { + case HUP: + return true + case INT: + return true + case KILL: + return true + case TERM: + return true + default: + return false + } +} + // Defines values for ProcessStatusState. const ( Exited ProcessStatusState = "exited" Running ProcessStatusState = "running" ) +// Valid indicates whether the value is a known member of the ProcessStatusState enum. +func (e ProcessStatusState) Valid() bool { + switch e { + case Exited: + return true + case Running: + return true + default: + return false + } +} + // Defines values for ProcessStreamEventEvent. const ( Exit ProcessStreamEventEvent = "exit" ) +// Valid indicates whether the value is a known member of the ProcessStreamEventEvent enum. +func (e ProcessStreamEventEvent) Valid() bool { + switch e { + case Exit: + return true + default: + return false + } +} + // Defines values for ProcessStreamEventStream. const ( Stderr ProcessStreamEventStream = "stderr" Stdout ProcessStreamEventStream = "stdout" ) +// Valid indicates whether the value is a known member of the ProcessStreamEventStream enum. +func (e ProcessStreamEventStream) Valid() bool { + switch e { + case Stderr: + return true + case Stdout: + return true + default: + return false + } +} + // Defines values for DownloadDirZstdParamsCompressionLevel. const ( Best DownloadDirZstdParamsCompressionLevel = "best" @@ -109,12 +262,40 @@ const ( Fastest DownloadDirZstdParamsCompressionLevel = "fastest" ) +// Valid indicates whether the value is a known member of the DownloadDirZstdParamsCompressionLevel enum. +func (e DownloadDirZstdParamsCompressionLevel) Valid() bool { + switch e { + case Best: + return true + case Better: + return true + case Default: + return true + case Fastest: + return true + default: + return false + } +} + // Defines values for LogsStreamParamsSource. const ( Path LogsStreamParamsSource = "path" Supervisor LogsStreamParamsSource = "supervisor" ) +// Valid indicates whether the value is a known member of the LogsStreamParamsSource enum. +func (e LogsStreamParamsSource) Valid() bool { + switch e { + case Path: + return true + case Supervisor: + return true + default: + return false + } +} + // 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. @@ -409,19 +590,19 @@ type ProcessExecRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user"` + AsUser *string `json:"as_user,omitempty"` // Command Executable or shell command to run. Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd"` + Cwd *string `json:"cwd,omitempty"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec"` + TimeoutSec *int `json:"timeout_sec,omitempty"` } // ProcessExecResult Result of a synchronous command execution. @@ -469,7 +650,7 @@ type ProcessSpawnRequest struct { AsRoot *bool `json:"as_root,omitempty"` // AsUser Run the process as this user. - AsUser *string `json:"as_user"` + AsUser *string `json:"as_user,omitempty"` // Cols Initial terminal columns when allocate_tty is true. Cols *int `json:"cols,omitempty"` @@ -478,7 +659,7 @@ type ProcessSpawnRequest struct { Command string `json:"command"` // Cwd Working directory (absolute path) to run the command in. - Cwd *string `json:"cwd"` + Cwd *string `json:"cwd,omitempty"` // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` @@ -487,7 +668,7 @@ type ProcessSpawnRequest struct { Rows *int `json:"rows,omitempty"` // TimeoutSec Maximum execution time in seconds. - TimeoutSec *int `json:"timeout_sec"` + TimeoutSec *int `json:"timeout_sec,omitempty"` } // ProcessSpawnResult Information about a spawned process. @@ -508,7 +689,7 @@ type ProcessStatus struct { CpuPct *float32 `json:"cpu_pct,omitempty"` // ExitCode Exit code if the process has exited. - ExitCode *int `json:"exit_code"` + ExitCode *int `json:"exit_code,omitempty"` // MemBytes Estimated resident memory usage in bytes. MemBytes *int `json:"mem_bytes,omitempty"` @@ -556,12 +737,12 @@ type ProcessStreamEventStream string // RecorderInfo defines model for RecorderInfo. type RecorderInfo struct { // FinishedAt Timestamp when recording finished - FinishedAt *time.Time `json:"finished_at"` + FinishedAt *time.Time `json:"finished_at,omitempty"` Id string `json:"id"` IsRecording bool `json:"isRecording"` // StartedAt Timestamp when recording started - StartedAt *time.Time `json:"started_at"` + StartedAt *time.Time `json:"started_at,omitempty"` } // ScreenshotRegion defines model for ScreenshotRegion. @@ -664,22 +845,11 @@ type StopRecordingRequest struct { // TypeTextRequest defines model for TypeTextRequest. type TypeTextRequest struct { - // Delay Delay in milliseconds between keystrokes. Ignored when smooth is true. + // Delay Delay in milliseconds between keystrokes Delay *int `json:"delay,omitempty"` - // Smooth Use human-like variable keystroke timing instead of a fixed delay. - // When enabled, text is typed in word-sized chunks with variable - // intra-word delays and natural inter-word pauses. The delay field - // is ignored when smooth is true. - Smooth *bool `json:"smooth,omitempty"` - // Text Text to type on the host computer Text string `json:"text"` - - // TypoChance Probability (0.0-0.10) of a typo per character, which is then - // corrected with backspace. Requires smooth to be true. Set to 0 - // to disable. Typical human range is 0.02-0.05. - TypoChance *float32 `json:"typo_chance,omitempty"` } // WriteClipboardRequest defines model for WriteClipboardRequest. @@ -2831,7 +3001,7 @@ func NewDownloadDirZipRequest(server string, params *DownloadDirZipParams) (*htt if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2876,7 +3046,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2890,7 +3060,7 @@ func NewDownloadDirZstdRequest(server string, params *DownloadDirZstdParams) (*h if params.CompressionLevel != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "compression_level", runtime.ParamLocationQuery, *params.CompressionLevel); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "compression_level", *params.CompressionLevel, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2937,7 +3107,7 @@ func NewFileInfoRequest(server string, params *FileInfoParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2982,7 +3152,7 @@ func NewListFilesRequest(server string, params *ListFilesParams) (*http.Request, if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3067,7 +3237,7 @@ func NewReadFileRequest(server string, params *ReadFileParams) (*http.Request, e if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3263,7 +3433,7 @@ func NewStopFsWatchRequest(server string, watchId string) (*http.Request, error) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3297,7 +3467,7 @@ func NewStreamFsEventsRequest(server string, watchId string) (*http.Request, err var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "watch_id", runtime.ParamLocationPath, watchId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "watch_id", watchId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -3347,7 +3517,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3361,7 +3531,7 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content if params.Mode != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "mode", runtime.ParamLocationQuery, *params.Mode); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "mode", *params.Mode, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3410,7 +3580,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params != nil { queryValues := queryURL.Query() - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "source", params.Source, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3424,7 +3594,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Follow != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "follow", *params.Follow, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3440,7 +3610,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.Path != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "path", *params.Path, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3456,7 +3626,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques if params.SupervisorProcess != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "supervisor_process", *params.SupervisorProcess, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -3618,7 +3788,7 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -3665,7 +3835,7 @@ func NewProcessResizeRequestWithBody(server string, processId openapi_types.UUID var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -3701,7 +3871,7 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -3746,7 +3916,7 @@ func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -3782,7 +3952,7 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "process_id", processId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -3874,7 +4044,7 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) if params.Id != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "id", *params.Id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -8540,7 +8710,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZip(w http.ResponseWriter, r *http return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8574,7 +8744,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8582,7 +8752,7 @@ func (siw *ServerInterfaceWrapper) DownloadDirZstd(w http.ResponseWriter, r *htt // ------------- Optional query parameter "compression_level" ------------- - err = runtime.BindQueryParameter("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel) + err = runtime.BindQueryParameterWithOptions("form", true, false, "compression_level", r.URL.Query(), ¶ms.CompressionLevel, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "compression_level", Err: err}) return @@ -8616,7 +8786,7 @@ func (siw *ServerInterfaceWrapper) FileInfo(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8650,7 +8820,7 @@ func (siw *ServerInterfaceWrapper) ListFiles(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8698,7 +8868,7 @@ func (siw *ServerInterfaceWrapper) ReadFile(w http.ResponseWriter, r *http.Reque return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8793,7 +8963,7 @@ func (siw *ServerInterfaceWrapper) StopFsWatch(w http.ResponseWriter, r *http.Re // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -8818,7 +8988,7 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http // ------------- Path parameter "watch_id" ------------- var watchId string - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) return @@ -8852,7 +9022,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ return } - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8860,7 +9030,7 @@ func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Requ // ------------- Optional query parameter "mode" ------------- - err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode) + err = runtime.BindQueryParameterWithOptions("form", true, false, "mode", r.URL.Query(), ¶ms.Mode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err}) return @@ -8894,7 +9064,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req return } - err = runtime.BindQueryParameter("form", true, true, "source", r.URL.Query(), ¶ms.Source) + err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) return @@ -8902,7 +9072,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "follow" ------------- - err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow) + err = runtime.BindQueryParameterWithOptions("form", true, false, "follow", r.URL.Query(), ¶ms.Follow, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err}) return @@ -8910,7 +9080,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "path" ------------- - err = runtime.BindQueryParameter("form", true, false, "path", r.URL.Query(), ¶ms.Path) + err = runtime.BindQueryParameterWithOptions("form", true, false, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) return @@ -8918,7 +9088,7 @@ func (siw *ServerInterfaceWrapper) LogsStream(w http.ResponseWriter, r *http.Req // ------------- Optional query parameter "supervisor_process" ------------- - err = runtime.BindQueryParameter("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess) + err = runtime.BindQueryParameterWithOptions("form", true, false, "supervisor_process", r.URL.Query(), ¶ms.SupervisorProcess, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "supervisor_process", Err: err}) return @@ -8985,7 +9155,7 @@ func (siw *ServerInterfaceWrapper) ProcessKill(w http.ResponseWriter, r *http.Re // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9010,7 +9180,7 @@ func (siw *ServerInterfaceWrapper) ProcessResize(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9035,7 +9205,7 @@ func (siw *ServerInterfaceWrapper) ProcessStatus(w http.ResponseWriter, r *http. // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9060,7 +9230,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdin(w http.ResponseWriter, r *http.R // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9085,7 +9255,7 @@ func (siw *ServerInterfaceWrapper) ProcessStdoutStream(w http.ResponseWriter, r // ------------- Path parameter "process_id" ------------- var processId openapi_types.UUID - err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "uuid"}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) return @@ -9126,7 +9296,7 @@ func (siw *ServerInterfaceWrapper) DownloadRecording(w http.ResponseWriter, r *h // ------------- Optional query parameter "id" ------------- - err = runtime.BindQueryParameter("form", true, false, "id", r.URL.Query(), ¶ms.Id) + err = runtime.BindQueryParameterWithOptions("form", true, false, "id", r.URL.Query(), ¶ms.Id, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) return @@ -11989,10 +12159,13 @@ func (sh *strictHandler) TakeScreenshot(w http.ResponseWriter, r *http.Request) var body TakeScreenshotJSONRequestBody 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 + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.TakeScreenshot(ctx, request.(TakeScreenshotRequestObject)) @@ -12872,10 +13045,13 @@ func (sh *strictHandler) DeleteRecording(w http.ResponseWriter, r *http.Request) var body DeleteRecordingJSONRequestBody 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 + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.DeleteRecording(ctx, request.(DeleteRecordingRequestObject)) @@ -12953,10 +13129,13 @@ func (sh *strictHandler) StartRecording(w http.ResponseWriter, r *http.Request) var body StartRecordingJSONRequestBody 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 + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.StartRecording(ctx, request.(StartRecordingRequestObject)) @@ -12984,10 +13163,13 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { var body StopRecordingJSONRequestBody 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 + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body } - request.Body = &body handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.StopRecording(ctx, request.(StopRecordingRequestObject)) @@ -13012,158 +13194,154 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/typinghumanizer/typinghumanizer.go b/server/lib/typinghumanizer/typinghumanizer.go deleted file mode 100644 index 66b00790..00000000 --- a/server/lib/typinghumanizer/typinghumanizer.go +++ /dev/null @@ -1,167 +0,0 @@ -package typinghumanizer - -import ( - "math/rand" - "strings" - "time" - "unicode" -) - -// UniformJitter returns a random duration in [baseMs-jitterMs, baseMs+jitterMs], -// clamped to a minimum of minMs. -func UniformJitter(rng *rand.Rand, baseMs, jitterMs, minMs int) time.Duration { - ms := baseMs - jitterMs + rng.Intn(2*jitterMs+1) - if ms < minMs { - ms = minMs - } - return time.Duration(ms) * time.Millisecond -} - -// SplitWordChunks splits text into word-sized chunks, keeping trailing -// whitespace and punctuation attached to the preceding word. -// For example, "Hello world. How are you?" becomes: -// -// ["Hello ", "world. ", "How ", "are ", "you?"] -func SplitWordChunks(text string) []string { - if len(text) == 0 { - return nil - } - - var chunks []string - var current strings.Builder - - runes := []rune(text) - i := 0 - for i < len(runes) { - r := runes[i] - current.WriteRune(r) - i++ - - if unicode.IsSpace(r) { - for i < len(runes) && unicode.IsSpace(runes[i]) { - current.WriteRune(runes[i]) - i++ - } - chunks = append(chunks, current.String()) - current.Reset() - } - } - - if current.Len() > 0 { - chunks = append(chunks, current.String()) - } - - return chunks -} - -// IsSentenceEnd returns true if the chunk ends with sentence-ending punctuation -// (before any trailing whitespace). -func IsSentenceEnd(chunk string) bool { - trimmed := strings.TrimRightFunc(chunk, unicode.IsSpace) - if len(trimmed) == 0 { - return false - } - last := trimmed[len(trimmed)-1] - return last == '.' || last == '!' || last == '?' -} - -// TypoKind identifies the type of typo to inject. -type TypoKind int - -const ( - TypoAdjacentKey TypoKind = iota // Hit a neighboring key - TypoDoubling // Type the character twice - TypoTranspose // Swap current and next character - TypoExtraChar // Insert a random adjacent key before the correct one -) - -// Typo describes a single typo at a position in the text. -type Typo struct { - Pos int // Character index in the rune slice - Kind TypoKind // What kind of typo -} - -// qwertyAdj maps each lowercase letter to its adjacent keys on a QWERTY layout. -var qwertyAdj = [26][]byte{ - {'q', 'w', 's', 'z'}, // a - {'v', 'g', 'h', 'n'}, // b - {'x', 'd', 'f', 'v'}, // c - {'s', 'e', 'r', 'f', 'c', 'x'}, // d - {'w', 's', 'd', 'r'}, // e - {'d', 'r', 't', 'g', 'v', 'c'}, // f - {'f', 't', 'y', 'h', 'b', 'v'}, // g - {'g', 'y', 'u', 'j', 'n', 'b'}, // h - {'u', 'j', 'k', 'o'}, // i - {'h', 'u', 'i', 'k', 'n', 'm'}, // j - {'j', 'i', 'o', 'l', 'm'}, // k - {'k', 'o', 'p'}, // l - {'n', 'j', 'k'}, // m - {'b', 'h', 'j', 'm'}, // n - {'i', 'k', 'l', 'p'}, // o - {'o', 'l'}, // p - {'w', 'a'}, // q - {'e', 'd', 'f', 't'}, // r - {'a', 'w', 'e', 'd', 'x', 'z'}, // s - {'r', 'f', 'g', 'y'}, // t - {'y', 'h', 'j', 'i'}, // u - {'c', 'f', 'g', 'b'}, // v - {'q', 'a', 's', 'e'}, // w - {'z', 's', 'd', 'c'}, // x - {'t', 'g', 'h', 'u'}, // y - {'a', 's', 'x'}, // z -} - -// AdjacentKey returns a random QWERTY neighbor of the given character. -// If the character has no known neighbors (non-letter), it returns the -// character itself unchanged. -func AdjacentKey(rng *rand.Rand, ch rune) rune { - lower := unicode.ToLower(ch) - if lower < 'a' || lower > 'z' { - return ch - } - neighbors := qwertyAdj[lower-'a'] - if len(neighbors) == 0 { - return ch - } - adj := rune(neighbors[rng.Intn(len(neighbors))]) - if unicode.IsUpper(ch) { - adj = unicode.ToUpper(adj) - } - return adj -} - -// GenerateTypoPositions computes typo positions using geometric gap sampling. -// O(typos) random calls, not O(chars). Returns a sorted slice of Typo structs. -func GenerateTypoPositions(rng *rand.Rand, textLen int, typoRate float64) []Typo { - if typoRate <= 0 || textLen <= 1 { - return nil - } - avgGap := int(1.0 / typoRate) - if avgGap < 2 { - avgGap = 2 - } - - var typos []Typo - halfGap := avgGap / 2 - if halfGap < 1 { - halfGap = 1 - } - pos := halfGap + rng.Intn(avgGap) - for pos < textLen { - roll := rng.Intn(100) - var kind TypoKind - switch { - case roll < 60: - kind = TypoAdjacentKey - case roll < 80: - kind = TypoDoubling - case roll < 95: - kind = TypoTranspose - default: - kind = TypoExtraChar - } - typos = append(typos, Typo{Pos: pos, Kind: kind}) - pos += halfGap + rng.Intn(avgGap) - } - return typos -} diff --git a/server/lib/typinghumanizer/typinghumanizer_test.go b/server/lib/typinghumanizer/typinghumanizer_test.go deleted file mode 100644 index 5271aa80..00000000 --- a/server/lib/typinghumanizer/typinghumanizer_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package typinghumanizer - -import ( - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUniformJitter(t *testing.T) { - rng := rand.New(rand.NewSource(42)) - - t.Run("stays within range", func(t *testing.T) { - for i := 0; i < 1000; i++ { - d := UniformJitter(rng, 100, 30, 50) - ms := d.Milliseconds() - assert.GreaterOrEqual(t, ms, int64(50), "should be >= minMs") - assert.LessOrEqual(t, ms, int64(130), "should be <= baseMs+jitterMs") - } - }) - - t.Run("clamps to minimum", func(t *testing.T) { - for i := 0; i < 100; i++ { - d := UniformJitter(rng, 10, 20, 5) - assert.GreaterOrEqual(t, d.Milliseconds(), int64(5)) - } - }) - - t.Run("zero jitter returns base", func(t *testing.T) { - d := UniformJitter(rng, 100, 0, 0) - assert.Equal(t, 100*time.Millisecond, d) - }) -} - -func TestSplitWordChunks(t *testing.T) { - tests := []struct { - name string - input string - expected []string - }{ - { - name: "simple sentence", - input: "Hello world", - expected: []string{"Hello ", "world"}, - }, - { - name: "with punctuation", - input: "Hello world. How are you?", - expected: []string{"Hello ", "world. ", "How ", "are ", "you?"}, - }, - { - name: "single word", - input: "Hello", - expected: []string{"Hello"}, - }, - { - name: "empty string", - input: "", - expected: nil, - }, - { - name: "only spaces", - input: " ", - expected: []string{" "}, - }, - { - name: "multiple spaces between words", - input: "Hello world", - expected: []string{"Hello ", "world"}, - }, - { - name: "trailing space", - input: "Hello ", - expected: []string{"Hello "}, - }, - { - name: "leading space", - input: " Hello", - expected: []string{" ", "Hello"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := SplitWordChunks(tt.input) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestIsSentenceEnd(t *testing.T) { - tests := []struct { - chunk string - expected bool - }{ - {"world. ", true}, - {"you?", true}, - {"wow! ", true}, - {"Hello ", false}, - {"word", false}, - {"", false}, - {" ", false}, - {"end.", true}, - } - - for _, tt := range tests { - t.Run(tt.chunk, func(t *testing.T) { - assert.Equal(t, tt.expected, IsSentenceEnd(tt.chunk)) - }) - } -} - -func TestAdjacentKey(t *testing.T) { - rng := rand.New(rand.NewSource(42)) - - t.Run("returns a neighbor for lowercase letters", func(t *testing.T) { - for i := 0; i < 100; i++ { - adj := AdjacentKey(rng, 'a') - assert.Contains(t, []rune{'q', 'w', 's', 'z'}, adj) - } - }) - - t.Run("preserves uppercase", func(t *testing.T) { - for i := 0; i < 100; i++ { - adj := AdjacentKey(rng, 'A') - assert.Contains(t, []rune{'Q', 'W', 'S', 'Z'}, adj) - } - }) - - t.Run("returns same char for non-letters", func(t *testing.T) { - assert.Equal(t, '5', AdjacentKey(rng, '5')) - assert.Equal(t, '.', AdjacentKey(rng, '.')) - assert.Equal(t, ' ', AdjacentKey(rng, ' ')) - }) -} - -func TestGenerateTypoPositions(t *testing.T) { - rng := rand.New(rand.NewSource(42)) - - t.Run("zero rate returns nil", func(t *testing.T) { - assert.Nil(t, GenerateTypoPositions(rng, 100, 0)) - }) - - t.Run("short text returns nil or few typos", func(t *testing.T) { - typos := GenerateTypoPositions(rng, 1, 0.05) - assert.Nil(t, typos) - }) - - t.Run("positions are within bounds and sorted", func(t *testing.T) { - textLen := 200 - typos := GenerateTypoPositions(rng, textLen, 0.03) - for i, typo := range typos { - assert.GreaterOrEqual(t, typo.Pos, 0) - assert.Less(t, typo.Pos, textLen) - if i > 0 { - assert.Greater(t, typo.Pos, typos[i-1].Pos, "positions must be strictly increasing") - } - } - }) - - t.Run("roughly matches expected count", func(t *testing.T) { - textLen := 1000 - rate := 0.03 - totalTypos := 0 - runs := 200 - for i := 0; i < runs; i++ { - localRng := rand.New(rand.NewSource(int64(i))) - typos := GenerateTypoPositions(localRng, textLen, rate) - totalTypos += len(typos) - } - avgTypos := float64(totalTypos) / float64(runs) - expected := float64(textLen) * rate - assert.InDelta(t, expected, avgTypos, expected*0.3, "average typo count should be near expected") - }) - - t.Run("kind distribution is weighted", func(t *testing.T) { - counts := map[TypoKind]int{} - for i := 0; i < 500; i++ { - localRng := rand.New(rand.NewSource(int64(i))) - typos := GenerateTypoPositions(localRng, 500, 0.05) - for _, typo := range typos { - counts[typo.Kind]++ - } - } - total := counts[TypoAdjacentKey] + counts[TypoDoubling] + counts[TypoTranspose] + counts[TypoExtraChar] - require.Greater(t, total, 0) - adjPct := float64(counts[TypoAdjacentKey]) / float64(total) - assert.InDelta(t, 0.60, adjPct, 0.10, "adjacent key should be ~60%%") - }) -} diff --git a/server/openapi.yaml b/server/openapi.yaml index 737e1db6..79396d80 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1367,26 +1367,9 @@ components: description: Text to type on the host computer delay: type: integer - description: Delay in milliseconds between keystrokes. Ignored when smooth is true. + description: Delay in milliseconds between keystrokes minimum: 0 default: 0 - smooth: - type: boolean - description: | - Use human-like variable keystroke timing instead of a fixed delay. - When enabled, text is typed in word-sized chunks with variable - intra-word delays and natural inter-word pauses. The delay field - is ignored when smooth is true. - default: false - typo_chance: - type: number - description: | - Probability (0.0-0.10) of a typo per character, which is then - corrected with backspace. Requires smooth to be true. Set to 0 - to disable. Typical human range is 0.02-0.05. - minimum: 0 - maximum: 0.10 - default: 0 additionalProperties: false ClipboardContent: type: object