Repository navigation
release: 0.9.0 — audited (rounds 1-5b), #[non_exhaustive] break, Linux host-crash root-cause fix - #72
Merged
Merged
Conversation
…hrough the bridge - on_main: a caller already on the Tauri main thread (a sync command, a menu handler, or a nested bridge call) now runs inline instead of spinning on MAIN_DISPATCH_LOCK — which could deadlock against a background holder whose closure is queued behind it, freezing the UI for the full 10s timeout. The main thread is recorded in plugin setup (which Tauri runs there). - tools.rs: victauri_eval_js / victauri_dom_snapshot called `webview.eval` directly from a tokio worker; they now go through WebviewBridge::eval_webview (main thread + serialized, like every other round trip). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…nistic default window Default-implemented trait additions (non-breaking for implementors): - try_list_window_labels distinguishes "could not ask the UI thread" from "no windows" — an empty list from a wedged UI is not evidence a window closed. - eval_webview_resolved returns the label the script was actually delivered to. find_window's fallbacks no longer depend on HashMap iteration order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…window Red-team (pre-audit) findings on the eval path: - Pending eval ids lived on a page-visible global (window.__VIC_EVAL__): page script could enumerate them and forge or suppress results. The watchdog and settle state now live in the bridge's closure (frozen, non-configurable API that never reveals an id); serialization uses JSON.stringify captured at init, before any page script. - A main-thread listing that timed out returned [] and was read as "window closed ... it most likely ran" — a busy UI now just keeps waiting; the check runs in its own task so it can't stall the wait past its deadline. - The watched window is the one the script was actually delivered to (eval_webview_resolved), so an unlabeled eval whose default window closes is detected (it was resolved lazily 1s later, to a different live window). - A trailing `// comment` in user code commented out the wrapper and turned the eval into a parse error; the code is now followed by a newline. - Page-originated evals (victauri_eval_js from page JS) get their own 10-slot budget so they cannot starve agent evals of the shared pending map. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…t_pub_crate) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g identity pinning, docs honesty CI / release - release-vscode: split into a secret-free build job (persist-credentials: false, npm ci --ignore-scripts, uploads the .vsix) and an environment-gated publish job that only runs when actually publishing; each token scoped to its one step; a real manual publish must be dispatched on a vscode-v* tag (dry runs work from any branch). - surface-audit: checks all six crates vs the workspace version; an unknown/failed registry query now fails the job with its own message instead of passing silently. - e2e.yml: kept (ci.yml does not run the victauri-test client suites) and fixed: explicit `toolchain: stable` for the SHA-pinned action, prebuilt binary, a `curl -sf` health retry loop instead of `sleep 10`, stderr capture, cleanup. - release.yml: previous tag via `git describe --match 'v[0-9]*' TAG^` (backport-safe). - docs.yml: pages/id-token permissions only on the deploy job. - deny.toml: unknown-registry / unknown-git = deny (cargo deny check sources: ok). - bump-version.sh: portable (no grep -P, sed -i.bak + rm); bump-version.ps1: root is exactly the parent of scripts/. - LICENSE copied into every published crate (cargo package --list includes it). victauri init - generated workflow: `permissions: contents: read`, persist-credentials: false, SHA-pinning guidance for the tag-pinned victauri-test action. - .mcp.json built with serde_json (hostile identifiers are escaped, not spliced). - never writes through a symlink: new files use create_new after a symlink check; the CLAUDE.md append refuses symlinks / non-regular files. Tests added. victauri-watchdog - identity pinning: once resolved, only discovery entries with that identity are followed; a dead watched pid is a failure even if another app answers the port; with --app/VICTAURI_APP and no match it reports the app DOWN instead of polling the default port. - discovery trust: base + entry dirs must be owned by the current euid (Unix, no unsafe) and not group/other-writable; Windows rejects symlinks/junctions. Tests. Docs: README/tools-reference action names regenerated from the real enums (+ route/trace/animation), overclaims corrected (latency, "identical on all platforms", dev-dependency advice, WebKitGTK CDP), tools-reference semantics for explain/db_health/replay/trace/fault/route, stale CLAUDE.md present-tense sections. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…ck "pending" Red-team (pre-audit) findings on the injected bridge: - Log getters returned the LIVE internal arrays: page script could push a forged "successful" IPC entry (which recording.replay would then invoke), splice out its own traffic, or plant values that broke every later read. They now return per-entry copies; ipcCommandPath tolerates non-strings. - The compact DOM snapshot wrote page text raw, so a newline in rendered text (an RSS title, a chat message) forged `[eN] button "…"` lines and could steer an agent's click. Page-derived strings are now JSON-encoded. - "Victauri-internal" traffic was detected by substring anywhere in the URL, so any request with `plugin%3Avictauri%7C` in its query escaped route rules and network logging; it's now decided from the parsed IPC command path. - IPC events are keyed by completion time (seq_ts) and, in watermark mode, a pending call is emitted once when it completes — a command slower than the drain gap used to stay "pending" forever (and replay then skipped it). The Rust drain advances by seq_ts and clamps the watermark to now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…internals pulled back
One deliberate semver break so future field additions are non-breaking.
No runtime behavior change except the victauri-test rediscovery fix below.
victauri-core
- #[non_exhaustive] (output-only): DomElement, ElementBounds, DomSnapshot,
ScoredCommand, RefHandle, MemoryDelta, VerificationResult, Divergence,
GhostCommandReport, GhostCommand, IpcIntegrityReport, StaleCall, ErrorCall,
AssertionResult, StateCheckpoint; enums InteractionKind, CodegenStyle.
- #[non_exhaustive] + constructors: IpcCall::new, WindowState::new + with_*
setters, SemanticAssertion::new, RecordedSession::new, RecordedEvent::new,
AppEvent struct variants (+ AppEvent::{state_change, dom_mutation,
dom_interaction, window_event, console}).
- CommandInfo/CommandArg #[non_exhaustive]; new builders (with_plugin,
with_args, with_arg, with_return_type, with_async, with_examples,
with_example; CommandArg::new/with_schema). #[inspectable] now expands to
the builder chain instead of struct literals.
- acquire_lock/read/write and the security/middleware modules #[doc(hidden)]
(still used by victauri-plugin and victauri-cli).
victauri-plugin
- #[non_exhaustive]: VictauriState (+ #[doc(hidden)] for_tests()),
PrivacyConfig, PrivacyProfile, CommandTimingStats, FaultType, FaultConfig
(+ new), JsonShape, ContractBaseline (+ new), ContractDrift, TypeChange,
StartupPhase, CapturedTauriEvent (+ new), TrackedTaskInfo,
ChildProcessInfo, TraceFrame, DbCandidate.
- filmstrip -> pub(crate); js_bridge, screencast, database, PendingCallbacks,
privacy::{observe,test,strict}_privacy_config -> #[doc(hidden)];
mcp::parse_bridge_event_from -> pub(super).
- Auth-token startup log: char-safe 4-char prefix + length (no byte slicing,
no 48-bit token leak). Privacy matrix comment corrected (Test/Observe are
deny-by-default).
victauri-test
- #[non_exhaustive]: PluginInfo, PluginToolInfo (now re-exported),
MemoryStats, SmokeCheckResult, SmokeReport, CoverageReport, CommandCalls,
LocatorMatch, Bounds, VisualDiff, ThresholdPreset. CheckResult::new and
VerifyReport::new added (types kept exhaustive: victauri-cli builds them).
- Rediscovery pins app identity: discover() records the app identifier
(/info app_identifier, else discovery metadata); rediscovery only accepts
a live server with that identifier, never falls back to port 7373, and now
also runs once on a connection error for discovered clients. Unit tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed db_health - query_db: an SQLite AUTHORIZER is now the enforcement point (default-deny: reads, functions, recursive CTEs, allowlisted PRAGMAs; a PRAGMA argument only for read-with-argument pragmas). The string checks missed table-valued pragma functions (`SELECT * FROM pragma_optimize` ran) and the parenthesized write form (`PRAGMA user_version(5)`); both are now refused before running. - Both paths treat the DB file as untrusted: trusted_schema=OFF + defensive. - db_health: the interrupt watchdog parks/unparks instead of sleeping 25ms steps that the drop joined (~25ms per table, capping a 5s budget at ~200 tables); the metadata + listing phase is budgeted too; virtual tables are listed but never counted (counting runs module code); a failed count sets row_counts_complete=false with a per-table count_error; the report says the integrity check is quick_check (integrity_check_kind). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…g by id - The background drain and `recording flush` kept separate watermarks and re-recorded each other's events. Both now go through one routine with a shared per-window watermark + per-window lock (VictauriState.drain_watermarks); flush with no label drains every window. Starting or importing a recording resets the floor to now, so an imported session's old start time no longer pulls the page's history in. - trace: ownership is the session id it started, not a flag. Restarting a trace ends the previous trace's recording instead of orphaning it (with its drain loop); stopping a trace never stops a recording someone else started; the 30-min auto-stop claims its generation atomically, so it cannot stop a newer trace. - victauri-test CheckResult/VerifyReport are now #[non_exhaustive]; the CLI uses their constructors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…d team - Browser-originated POSTs must be JSON (415 otherwise): a page on any localhost origin could fire CORS-simple, no-preflight tool calls at an auth_disabled() server. Non-browser clients (no Origin) are unaffected. - One GLOBAL concurrency cap: axum instantiated the per-route layer per route, giving /mcp and /api/tools 64 slots each. - victauri bridge (Unix): liveness counts only our own user's processes (`ps -U <euid>`); `ps -A` let another user's recycled PID keep a dead app's discovery entry "alive" for a port squatter. - query_db absolute paths: containment is decided lexically before touching the path, so errors no longer reveal whether an arbitrary file exists. - eval auto-return: postfix ++/--, `a\n++b`, regex literals (incl. quotes inside them), keyword operators at line end, U+2028/2029, and a `;` followed only by a comment are now handled per JavaScript's ASI rules. - Tool annotations now match what the tools can do: recording (replay), introspect (contract_record/check invoke commands; *_clear), logs (clear), window (close) are destructive; verify_state/wait_for/assert_semantic run caller JS; inspect/animation mutate the page. Descriptions for trace, fault, explain, db_health and logs corrected. - tools_fallback.json (shown while the app is down) regenerated; a test now pins its names AND descriptions to the live tool router. - WebviewBridge documents its stability contract for implementors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
0.9.0 is a deliberate one-time semver break (public data types become #[non_exhaustive] with constructors) carrying two audit rounds of fixes. Not published: the adversarial audit runs first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
… code under --no-default-features) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…id" while the page reloads Found by the live-4DA soak (45,905 calls, 6 reloads, 1 error): selector validation used document.body.matches(), so a null body during a reload threw a TypeError that was reported as "invalid CSS selector: button". Validation now uses a detached fragment, and a missing body reports "page not ready". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
Found by the live-4DA soak: an eval in flight when its page reloaded (HMR, a navigation) waited out the full 30s timeout and then only guessed "…or the webview reloaded". Every bridge (re)init already sends a ready signal; the eval callback now records the load time per window (label from Tauri — page script cannot affect another window), and an eval whose window loads a new page after it was injected returns "window 'X' loaded a new page while the call was in flight … check the app's state before re-running it". A 250ms grace absorbs the fire-and-forget ready signal of the page the eval runs in, so a late signal is never mistaken for a reload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
Found by the live-4DA dogfood run: 4DA's get_blind_spots takes up to 57s cold, so with the fixed 30s eval timeout an agent could not call it at all. invoke_command now accepts timeout_ms (max 300000), a timeout error points at it, and timeouts/app-exit/closed-window/reload outcomes are no longer recorded as command durations (they skewed p95). Timeout messages show ms under 1s. Changelog/migration updated with the live-test findings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
CI (windows-latest) failed connection_refused_on_discovered_client_rediscovers_ same_app_only: closed_port() bound then released a port, another process on the runner took it and answered with non-HTTP bytes (hyper Parse(Version)) instead of refusing. The helper now keeps the socket bound but not listening for the test's lifetime, which refuses connections deterministically on Windows and Linux. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw
…capability gate
serde accepts an externally-tagged enum ({"action":{"go_to":null}}) and, on
REST, a positional array body. Both used to resolve to the bare tool name at the
gate -- which the Test profile allows for navigate -- while the handler still
parsed and ran go_to; with FullControl they bypassed per-action disabled_tools.
The gate now refuses any call it cannot map to exactly one known action.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
Tauri camelCases command arguments by default (size_kb -> sizeKb), so an agent passing the registry's Rust names got "missing required key" -- including on the gauntlet app's own large_payload. CommandArg gains an optional `key` (+ with_key / invoke_key); the macro derives it with heck exactly as tauri-macros does, honours rename_all / rename (as #[inspectable] options, or read from a #[tauri::command(...)] it can see), unraws r#idents, and detects injected framework args by the type path's last segment (State<'_, a::B> was reported as a user argument; Request/CommandScope/GlobalScope were missing). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
Recorded page text reached generated Rust with bidi/control characters intact,
which rustc rejects (text_direction_codepoint_in_{comment,literal} are
deny-by-default); they are now \u{...}-escaped. Recorded selectors are parsed by
shape from the start, with CSS string/ident escapes decoded, instead of by
substring search: a test id containing ':has-text("' no longer masquerades as
a text selector, '#a.b' / '#a:hover' are no longer treated as the id 'a.b', and
CSS-escaped ids resolve to the real id. Scrolling to text uses the new
scroll_to_by_text rather than passing the text as a CSS selector.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…s error check/uncheck assigned el.checked and dispatched synthetic events, which React's value tracker swallows: measured on React 18, onChange fired 0 times and the component reverted. They now click() when the state differs and verify the result (erroring on a disabled box or a checked radio). A ref that went stale is now ElementNotFound instead of reading as empty text / unchecked / no attribute. get_attribute encodes the name as a JSON string. Visual baselines decode 16-bit, indexed and gray+alpha PNGs (normalize_to_color8) instead of misreading or refusing them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…h, LIKE patterns and lock waits Audit F1/F2(db side)/F7. - The 5 MB result cap was checked only after a whole row had been materialized, base64'd and re-serialized; with 1 MB cells and SQLite's 2000-column default one row could reach gigabytes. Each cell's encoded size is now computed from the raw value (a blob's base64 length is arithmetic) and charged before conversion; the re-serialization is gone and result_bytes stays exact. - SQLITE_LIMIT_COLUMN = 256 for the query's result set. The limit also bounds table definitions, so the schema is loaded first under the default - otherwise one wide table made the whole database unqueryable. - Duplicate result names (SELECT 1 AS a, 2 AS a) silently dropped a value from the row object; repeated names now get a `:N` suffix and `columns` lists exactly the keys each row carries. - SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 1000 on the query and db_health connections: LIKE/GLOB never check for an interrupt. - busy_timeout 5s -> 1s: the lock wait is added to the CPU deadline while the query holds a read transaction that stalls WAL checkpoints. Also backticks a doc identifier clippy's doc_markdown rejects on the base. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…proof pending slots The eval lifecycle had several ways to tell an agent the wrong thing about code it sent: - Parse watchdog race (C1/V-5): a 750ms timer armed by one main-thread round trip raced the user code delivered by a second. A busy main thread delaying the code past 750ms reported "syntax/parse error" for code that then RAN (an invoke_command retry ran twice). The code is now delivered first and a check script after it (webview evals run in order): "not begun" means it did not parse. No timer. The bridge keeps tombstones of settled evals, so every outcome is delivered at most once and code reported as never begun cannot run. - Reload detection (C4, V-13): the 250ms grace hid real reloads that completed within it and misread a same-page ready signal delayed past it as a reload. Each page load now has a nonce (sent with the ready signal, reported by the liveness probe); a reload is a ready signal from a different page, confirmed by asking the page for its current nonce, so page script can no longer abort agent evals by forging the ready signal. bfcache restores re-announce the page. - Pending-slot leak (C2/N2): entries were removed only on paths inside the eval future, so a REST client that disconnected leaked its slot; 100 leaks wedged every eval tool. Every reservation (agent eval, probe, page-originated eval, recording drain) is now an RAII guard. - Typed eval outcome (C7): invoke_command timings came from an error-text prefix list and recorded calls that never reached the command (saturated map, dead bridge, failed injection, parse error). Failures are now typed NotSent / Aborted / Page; only calls that ran are timed. - App exit (C15d): subscribe() marks the current shutdown value seen, so an eval started after Exit waited its full timeout. It is now checked up front (and Exit uses send_replace so the value flips even with no receiver left). - Unserializable results (V-12): circular/BigInt results read "JavaScript error" and functions came back as a raw envelope; both are now "the code ran, but its result could not be serialized to JSON (<reason>)". - Page-eval budget (V-15): per window (10) under a page-wide cap (25), so one window cannot starve another's page-originated evals. Also fixes a pre-existing doc_markdown lint in a test comment that failed clippy --all-targets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
… a mocked flag - EventRecorder hands out a fresh generation per start/import (start_session, generation, record_event_if, stop_if_generation). A reader that captured the generation before a slow read can no longer append into a later recording, and an owner can no longer stop a later recording that reuses its session id. - stop_if_session checks the id and stops under ONE lock (a stop+start in between made it stop someone else's recording: 20/200k in the regression test). - import_if_idle: the is-recording check and the replace happen under one lock. - IpcCall gains `mocked` (serde default, omitted when false) + with_mocked(): a call a route rule answered in the page never reached the backend. Audit C6 (core half), C15a, C15b, V-3 (type half). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
The map is keyed by the caller-chosen command name, so ever-new names grew it forever. Once full, new names are not tracked; tracked commands keep accumulating. Audit C15e. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…nput only reaches the app - on_main (C15c): the `abandoned` flag was checked only when the closure started, so work that began at the deadline still ran after the caller reported a timeout, and the dispatch lock was released while the closure was still queued. The closure and a timing-out caller now race through one compare-and-swap (Queued -> Running vs Queued -> Abandoned): a timeout error means the work never ran and never will; work that already started is waited for (bounded) and its real outcome returned, with the dispatch lock held throughout. The logic lives in a small `dispatch_and_wait` seam so it is unit-tested without a Tauri runtime. - Windows trusted input (C3): the SetForegroundWindow result was ignored, then SendInput sent keystrokes/clicks to whatever had focus — Windows' foreground lock refuses focus while the user types elsewhere, so an agent's Enter could land in the developer's terminal. Input is now sent only after verifying GetAncestor(GetForegroundWindow(), GA_ROOT) is the target window, and a click only if WindowFromPoint at the target point resolves inside it; otherwise a clear error. `input press_key` with `trusted` and a `ref_id` now stops when the element cannot be focused instead of sending the key anyway. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…sy-UI or path oracles Audit F2 (tool side), F3-F6, C8, C10-C12. - F2: query_db / db_health run through bounded::run_blocking_bounded: a deadline (op budget + 5s slack) the caller never waits past, and a 2-slot semaphore whose permit is held by the blocking closure itself, so a timed-out call keeps its slot until its thread really ends. Residual: a single SQLite string op can still overrun its own deadline by seconds on the blocking thread; the caller no longer waits for it and at most 2 such threads exist. - F3: one ordered, de-duplicated (keep-first) root list for both tools; a HashSet made relative-path resolution nondeterministic and db_health skipped the log dir. - F4: list_app_dir walks on the blocking pool, canonicalizes the base once, and stops (truncated: true) at 10k returned / 100k examined entries or 5s. - F5: a read truncated mid-character drops the partial character instead of returning the whole file as base64. - F6: `..` in an absolute db path is refused; missing vs escaping-through- a-symlink answer identically in query_db, read_app_file and list_app_dir. - C8: look-back windows saturate to "all history" (Utc::now() - TimeDelta panicked; u64 -> i64 wrapped into a future baseline); REST execute_tool and MCP call_tool dispatch behind a catch-unwind boundary that turns a handler panic into an error result. - C10: wait_for(event) never sleeps past its deadline; poll_ms is clamped to [20, timeout_ms] before it reaches JS. - C11: WebviewBridge::try_get_window_states; window get_state/list/ introspectability, capture targeting, the windows resource, introspect capabilities and recording flush report "UI thread busy" instead of empty lists / "window not found". - C12: app_state probes run on the blocking pool with a 10s timeout; a panicking probe is a tool error. Updates db_path_outside_roots_gives_no_existence_oracle for the explicit `..` refusal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…true replay, trace lifecycle
Drain (C5/V-10, C13, C6):
- The JS bridge sequences each log entry the first time a drain sees it
complete (`drainEvents(afterSeq, instance, floorMs)` -> {instance, seq,
events}); the Rust drain position is (page instance, seq), not a wall-clock
watermark clamped to the Rust clock. A future-dated event was re-read on
every drain (1 event -> 5 records in 5 drains) and same-ms events pushed
after a read were lost; now each is read exactly once. A reload restarts the
sequence under a new instance id. Only a window's first read in a recording
skips pre-recording history by timestamp.
- Plain network events are keyed by completion and not emitted while pending
in watermark mode (same fix IPC got in R2-13).
- The drain pins the recorder generation before its read and records with
record_event_if; positions are per recording epoch (= generation), a read
that straddles a reset cannot plant an old position, and a drain in the gap
between `start` and the epoch reset does not read. Start/import take the
floor before the recording becomes active; import is check-and-replace
atomic (import_if_idle).
Replay / mocked calls (V-3):
- ipc/network stream events carry `mocked` for route-fulfilled/blocked
requests; the drain records it on IpcCall and replay skips mocked calls.
- Replay runs each call in the window that recorded it (never a default
window; a gone window fails the call); `webview_label` now filters.
- The drain reply is serialized by the bridge's pristine JSON.stringify via
`_evalSettle`, not the page's.
Page JSON (V-4):
- mcp::page_json::{sanitize_lone_surrogates, parse_page_json}: escape-aware
rewrite of unpaired \uD800-\uDFFF escapes to �. Used by the drain, the
eval envelope (so every eval_with_return consumer: introspect
command_timings/coverage/command_catalog, explain) and those sites directly.
The drain's position comes from the bridge counter, so an unparseable entry
no longer stalls a window.
Trace (C14):
- active/generation/owned recording live under one mutex; ownership is by
trace generation + recorder generation (no ABA via session ids); an old
task's auto-stop can no longer take a newer trace's recording.
- interval clamped to 50..=60000 ms so the 30-min auto-stop keeps firing.
- CaptureTaskGuard ends the trace and its recording however the capture task
exits, including a panic.
Also: backticks for a pre-existing clippy doc_markdown hit in mod.rs tests;
tools_fallback.json regenerated for the recording description.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…ge e2e isolated Windows liveness spawned tasklist and substring-matched its output: PID 12 'matched' 123, other users' processes counted (the cross-user PID-reuse fix R2-7 only reached Unix), victauri-test resolved tasklist via PATH, and the bridge ran a full tasklist snapshot every 1.5s even with no discovery entries. Measured under load: 0.5s per call, up to 40s when filtered by user -- which stalled the bridge's own replies and made bridge_e2e fail 3 of 7 runs. Liveness is now OpenProcess + exit code + token-SID == ours (victauri-test process module, copied into the watchdog; unsafe confined to that module), and scans skip enumeration when nothing was discovered. The e2e tests write their fake discovery entries into a private temp root instead of the real one, where a developer's own victauri bridge connected to the mock mid-test (observed). bridge_e2e: 12/12 green, ~2s each. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We4pMEPqPFqE4WVQy4GWGz
…yed (R5B-TERM3) `TestError::ToolError` and `TestError::Mcp` messages often carry page-derived text (a JS exception message) and were printed raw by their Display impls: an ESC sequence could drive the terminal, and a line of its own starting with `::error ...` (or legacy `##[...]`) became a forged GitHub Actions annotation in a CI log. New `terminal::untrusted_multi_line` keeps legitimate line breaks, escapes control/bidi characters (as `multi_line` does), and escapes the first character of any line that would read as a workflow command; both Display impls use it. The stored message itself is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…t start (R5B-SMOKE1) `assert_recording_lifecycle` called `stop_recording()` first "to clean up", which ended -- and discarded -- any recording already in progress, e.g. a concurrent `victauri record` session. It now starts first (race-free: the app refuses a second session); if a recording is already active it leaves it running and does not run. The smoke suite reports that check as skipped with the reason (new `SmokeCheckResult::skipped` field, `[SKIP]` in the summary; a skip counts as passed). Once the check owns its recording it stops it on every path, so a failed eval no longer leaves it running. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…r PATH (R5B-BR7) On Unix the bridge had its own liveness: `ps -U` / `kill -0` resolved from /bin or /usr/bin, else a BARE name looked up on PATH. On NixOS/Guix/minimal images that meant a PATH lookup (a hijacked PATH decides which PIDs look alive, i.e. where the Bearer token goes), or no liveness at all and no app found. Per-PID liveness now calls `victauri_test::process::is_own_live_process` on every platform (the R4-hardened kill/sh-builtin//proc chain Windows already used). The batched `ps -U` snapshot is kept for speed but is resolved only from fixed locations incl. the NixOS/Guix system profiles, and is skipped (falling back to the hardened per-PID check) when absent -- never a bare-name lookup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…B-FETCH0) With no arguments the interceptor converted the missing input to the string "undefined" and fetched `./undefined` (logged as a real request) instead of letting native fetch reject with its TypeError. It now delegates a zero-arg call to the original fetch untouched, and forwards `fetch(x)` as one argument rather than growing an explicit `undefined` init. The jsdom harness's fetch stub now rejects a zero-argument call as native fetch does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
The stdio loop awaited each forward before reading the next line, so one long `tools/call` (the default HTTP timeout is now 330 s) delayed `ping`, every parallel tool call, and the `notifications/cancelled` meant to stop it. Now: - local methods (initialize, the client's notifications/initialized, ping, batches, invalid messages) are answered inline; - every forwarded request runs as its own task, bounded at 32 in flight (past that a request is refused at once with an error, never queued unbounded); - notifications (incl. cancelled) and client responses are forwarded promptly and in order by one dedicated task (bounded queue); - each forward holds its resolved backend in a local, and the lazy backend handshake is single-flight (a tokio mutex + re-check), so concurrent requests never race several handshakes; a stale reply clears the session only if it is still the one that request used (compare-and-clear). Kept: one complete JSON line per locked stdout write, the no-replay rule for a possibly-delivered tools/call, list_changed only after notifications/initialized. On stdin EOF in-flight requests get a bounded grace period to answer. E2E: two 4 s `slow` calls + ping + notifications/cancelled -- the ping is answered first and at once, the cancel reaches the backend while the call runs, and both calls finish in ~one call's time. (Before: the ping's reply came after the slow call.) The client-response test now polls for the async forward. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
… error (R5B-ISERR1)
interact, input, inspect and `route add` returned the page's
`{ok:false, error, hint}` (or `{error}`) as a SUCCESS result with isError
unset, so a refused click (covered, disabled, detached, ref not found) read
as done unless the caller parsed the body — VictauriClient/Locator
returned Ok, and the VS Code "Click element" command reported success.
They now return isError with the page's message and its recovery hint in
the standard `[hint: RETRY_LATER|CHECK_INPUT]` form (only an UPPER_SNAKE
hint word is carried over — it is page-controlled). Array results with
per-item errors (get_bounding_boxes) and results that merely contain an
`error` field below the top level stay successes.
Callers: victauri-test client/Locator need no change (they already map
isError to TestError::ToolError; a refusal is now Err instead of a silent
Ok). The demo-app adversarial stale-ref test now requires the tool error.
Tool descriptions + docs/tools-reference.md document the behavior; CLI
fallback manifest regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
`--app` bound a backend from discovery metadata + PID liveness only. After a crash the app's stale entry can carry a PID that another of our processes reused, and if a DIFFERENT Victauri app (auth disabled) now holds the port the bridge silently drove the wrong app (the residual bridge.rs documented). With an app selector, the resolved backend's own `/info` (app_identifier / app_product_name, compared exactly and ASCII case-insensitive like matches_app) must now match before anything is forwarded; a mismatch or a server that does not confirm its identity is treated as "not found" with a message naming what it reported, and nothing is sent to it. The confirmation is cached per resolved (pid, port, token), so the steady state costs no extra request per call, and is re-checked whenever the resolution changes (an entry without a pid -- a bare VICTAURI_PORT -- is re-checked every time). The e2e mocks now serve /info like the real plugin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…(R5B-PORTAPP1) With VICTAURI_PORT set, `--app` / VICTAURI_APP was silently ignored by both the bridge and victauri-test's discovery (so every other CLI command): the client drove whatever app held that port. When both are set they are now checked against each other: - bridge: the one live discovery entry on that port must match the selector (metadata), and the server's own /info identity must match too (the R5B-BR6 confirmation, cached per pid/port/token); otherwise the call is refused with "VICTAURI_PORT=N is app X, but the app selector ... is 'Y'" and nothing is forwarded. - victauri-test: `try_resolve_connection` now scans discovery when a selector is set even with an explicit port + token, and refuses a disagreeing entry; `discover_app` re-checks the selector against the reached server's /info (identifier or product name) and refuses a mismatch -- which also covers a port with no discovery entry. Shared `identity_matches` rule (exact, ASCII case-insensitive). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…ns (R5B-SQLFN1) The query_db authorizer allowed every SQL function, so a side-effecting function the host registers process-wide (sqlite3_auto_extension) ran despite the read-only open. The authorizer now applies a per-connection function policy computed from that connection's pragma_function_list: a name is allowed if every registration under it is built in, registered SQLITE_DETERMINISTIC, or one of SQLite's own extension functions (FTS3/4 snippet/offsets/matchinfo/match, FTS5 bm25/highlight/snippet/..., R-Tree, Geopoly). A non-deterministic app function - including one overriding a built-in name - is refused with an error naming it. load_extension and FTS3's optimize are never allowed. If pragma_function_list is unavailable the policy fails closed to a hard-coded list of SQLite built-ins; a test pins that list against the linked SQLite so a newer SQLite cannot drift. Also fixes a pre-existing break found by the new tests: R-Tree tables were unreadable through query_db (the R-Tree module prepares its shadow-table INSERT/UPDATE/DELETE when it connects, which the authorizer refused). Write preparation is now allowed on the R-Tree/Geopoly shadow tables only; the READ_ONLY open still stops any write from executing. The authorizer is now installed after the schema load, so a locked database fails once (within the 1 s busy timeout) instead of in every policy read. SetupOutcome keeps the install fail-closed across rusqlite 0.32..0.40. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…-DATAURL1) The network log (up to 1000 entries) kept every request URL verbatim, so an app that fetches large data: URLs pinned 1000 x their size: a jsdom PoC of 1000 fetches of a 1 MB data: URL retained 1,048,596,000 URL chars (~1 GB+); now 2,065,000. A logged URL longer than 2048 chars is cut (surrogate-safe, truncText) with a `…[+N chars]` marker; IPC URLs are never cut, and route matching still sees the full URL. Route matching also no longer resolves opaque (data:/blob:/javascript:/about:) or >8 KB URLs through the URL parser, which only cost time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…e code (R5B-QC1)
On SQLite >= 3.44 a whole-database PRAGMA quick_check connects every
virtual table whose module is registered and runs the module's integrity
routine - module code, including a module the host registered process-wide
(an auto-extension, e.g. a vector-search extension). Verified: with such a
module registered and a virtual table using it in the file, db_health ran
its constructor ("vtable constructor failed").
db_health now lists the connection's modules (pragma_module_list). If the
file has (or may have) virtual tables and a non-built-in module is
registered - or the list is unavailable - it runs PRAGMA quick_check(<t>)
per ordinary table instead, which connects no virtual table, and reports
integrity_check_kind "quick_check (per table)" with the reason in the new
integrity_check_note field (virtual tables and the free list are then not
checked). Otherwise the whole database is still checked, SQLite's own
FTS/R-Tree tables included; a vtab whose module is not loaded is not
connected. Budgets are unchanged (the per-table pass shares the check
budget). New integrity_check_note is null in the whole-database case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…o (R5B-EVALWIN1) victauri_eval_callback delivered a result for any pending eval id to whichever webview sent it. Every pending-eval reservation now records its target window (PendingSlot::bind_window: agent evals, liveness probes, recording drains, page evals), and the callback drops (with a debug log) a result whose calling webview is not that window. An eval sent to the default window (label None) is checked against the window the default resolves to when the result arrives, and is narrowed to the concrete label as soon as eval_webview_resolved reports it. A result for an unbound or unknown id is refused. The binding lives exactly as long as the slot. The bridge-ready signal is unchanged (it carries no eval id and records the load against the calling webview's own label). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
… JSON (R5B-REDACT1) Redaction is best effort, but two common leak paths were not covered: - Secrets in URL query strings/fragments (IPC/network logs carry full, often tokenized URLs). The value of access_token, refresh_token, id_token, auth_token, token, api_key/apikey, key, client_secret, secret, password, pwd, auth, sig/signature and the AWS/GCS signing params after ?, & or # is now replaced (whole parameter names only: ?keyboard= and &tokens_used= are untouched). - Key-based redaction ran only when the WHOLE output parsed as JSON. It now also follows JSON carried inside JSON string values (captured bodies are JSON-encoded strings; up to 4 levels), and redacts "key": value pairs in plain text and in truncated JSON fragments (incl. a value cut off at the end of the text). docs/src/security.md lists the new coverage and the remaining limits: text probes (find_elements text search even under Observe, wait_for text under Test, assert_semantic/eval_js under FullControl) can still confirm redacted content piecewise; secrets under innocuous keys, in non-JSON formats, or in URL paths are not recognised. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…s deterministic (R5B-FLAKY) Root causes, measured under 2x CPU oversubscription on 16 cores: - bridge `a_job_outliving_its_caller…` (and two siblings) posted the job to a freshly spawned stand-in "main thread" and gave the caller 50 ms. Under load the thread is scheduled after the deadline, the caller correctly abandons a job that never started, and the "job outlived its caller" precondition fails: 30/30 failures in a 30-run loop. A new `post_started` stand-in returns only once the job has signalled that it started, the first job now runs until explicitly released, and it is released only once the second caller holds the dispatch lock (provably waiting on it): no sleep decides any outcome. 0/30 after. - `app_state_probes_are_bounded_and_panic_isolated` and the two `read_app_file` tests ran EVERY probe/read under a 1 s `cfg(test)` deadline. A panicking probe needs a new blocking-pool thread (the stuck probe holds the first) plus the panic hook and unwind: ~0.4 s at 2x oversubscription (0.7 ms idle), more in a full parallel suite, so it was reported as "did not finish within 1000 ms" (reads: 2/30 failures). The deadlines are now per-handler fields with the unchanged production values (10 s / 15 s — no production deadline widened); only the tests that exercise the deadline itself shorten them. 0/30 after. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…ORIS) After R5-NET1 a silent connection no longer holds a request slot, but a local process could still fill all 256 slots with started-but-stalled request heads (one byte, then nothing), each held until the 30 s header deadline. Every client of this server sends a complete head in one write, so ServeLimits::DEFAULT now uses 10 s: still generous for a machine stalled by a debugger or load, and keeping every slot busy now takes three times the reconnect rate. The deadline is effectively measured from the first byte (hyper starts the timer once serve_hardened has admitted the connection on its first byte); it is also the idle keep-alive timeout, which HTTP clients handle by reconnecting. Slow-loris from a loopback process that keeps reconnecting stays bounded, not prevented. Existing tests now use the default deadline instead of a hard-coded 30 s; a stale "64 request slots" comment is corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…R5B-PRISTINE1)
The fetch interceptor parsed captured IPC request/response bodies (and a route
fulfill body) with the page's JSON.parse / JSON.stringify, so page script
replacing them forged the args/results that logs ipc, replay and the command
catalog report. They now use PRISTINE_PARSE / PRISTINE_STRINGIFY.
The in-app victauri_eval_js command (and victauri_dom_snapshot, same pattern)
built its result and callback args with the page's JSON and a plain object: a
throwing Object.prototype.toJSON made every page-originated eval hang until
its timeout, and a replaced JSON.stringify forged the result. The script is
now js_bridge::page_eval_script: the body comes from a new bridge method
_pageEvalBody (init-time stringify, blind to universal-prototype toJSON,
never throws: an unserializable value is reported as {"__error": ...}) and the
callback args are a null-prototype object, mirroring R5-JS2 for agent evals.
Result format is unchanged except that an `undefined` result now arrives as
`null` instead of never arriving. Also fixes a clippy doc_markdown nit in the
R5B-XHR1 test comment.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…ns (R5B-WINDISC1)
On Windows every discovery reader trusted any directory under
`%TEMP%\victauri`. When TEMP is shared (an app launched from MSYS2 uses
`C:\msys64\tmp`; a redirected `C:\Windows\Temp`), another user could plant
`victauri\<live pid>\{port,token,metadata.json}` pointing at a port they
control, and clients sent it agent calls and accepted forged results.
Readers now trust a root or `<pid>` directory on Windows only if it is a real
directory (no symlink/junction) whose owner SID is the token user, the token's
default owner, or BUILTIN\Administrators when this token is a member (enabled
or deny-only) -- the plugin writer's `acceptable_owner_sids` rule incl. 659e353:
- victauri-test: `process::dir_owned_by_current_user` (GetNamedSecurityInfoW +
token SIDs, next to the existing Win32 liveness code); discovery.rs uses it.
- CLI bridge: calls victauri-test's check (the crate forbids unsafe); the
documented "Windows ACL residual" is closed.
- watchdog: the same check in its process.rs copy.
- VS Code: TypeScript cannot call Win32, so a directory outside the user
profile is checked with one batched `Get-Acl` PowerShell call (absolute
powershell.exe path, paths passed via env, 10 s timeout, fails closed),
cached per directory identity (inode + birth time). A directory inside the
profile (the default %LOCALAPPDATA%\Temp) needs no check: Windows grants no
other non-admin user access to a profile, so it costs no process spawn.
Tests (Windows; SYSTEM stands in for "another user" via `icacls /setowner`,
skipped when not elevated): planted entry and planted root refused in each
reader; our own entries still found.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…ken over (R5B-LINDISC1) On Linux without a private XDG_RUNTIME_DIR the discovery root is the predictable /tmp/victauri-<euid>. Another local user can pre-create it; the app refuses it (not owned) and never registers, and sticky /tmp stops the victim deleting it -- a persistent discovery DoS. Writer (plugin mcp/server.rs, discovery_root only): the root is now decided once per process (OnceLock, so write and cleanup agree). When the primary root is untrusted or cannot be created, the entry goes to the per-user home state root -- $XDG_STATE_HOME/victauri, else $HOME/.local/state/victauri (absolute only) -- created 0700, verified with the same private-dir check, its parent created 0700 when missing and required to be ours and not other-writable. No usable fallback -> the primary is kept and fails closed as before. Readers (victauri-test discovery, CLI bridge, watchdog, VS Code) scan, in order: $XDG_RUNTIME_DIR/victauri, <temp>/victauri-<euid>, the home state root, then the legacy <temp>/victauri -- all with the existing ownership/permission checks. Doc comments, the token-location hint, the watchdog --help text and docs/src (security, getting-started, faq, configuration, architecture) describe the roots. Verified on WSL with a GENUINELY foreign-owned /tmp/.../victauri-1000 (chowned to nobody): before, the plugin's port_file_roundtrip failed with EACCES; after, the entry lands in $HOME/.local/state/victauri (0700) and the real `victauri invoke` and `victauri bridge --app` binaries find the app there. Unit tests cover the selection (symlinked / blocked primary), the XDG_STATE_HOME rule and the parent guard, and each reader's root order; a Unix CLI e2e finds an app registered only under the home root. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
# Conflicts: # crates/victauri-plugin/tests/bridge_r5b_tests.rs
…terministic tests, docs - Every Victauri HTTP client (CLI bridge, victauri-test, watchdog) drops pooled connections after 5 s, below the plugin's new 10 s idle keep-alive, so a pooled connection is never reused just as the server closes it (that loss would read as "possibly delivered" and a tool call would not be retried). - rate_limiter_concurrent_acquire (core + plugin): the fixed 1,200 ceiling failed 19/20 under CPU load; the bound is now 1,000 + 1 per elapsed ms, which only a refill double-count exceeds. - InterruptGuard counts watchdog wake-ups in test builds; interrupt_guard_parks_instead_of_polling pins the park-until-deadline behaviour deterministically (a 25 ms stepping watchdog wakes 11x in 300 ms and fails it). db_health's 400-table test sets up in one transaction (~75 s -> <2 s) and checks completeness under generous budgets; lock_wait_is_short asserts the configured busy_timeout; the LIKE/GLOB test's wall-clock bound never discriminated. - smoke: the skippable recording check goes through a skippable_check! macro; the doc-count lint counts both forms (it read 10 checks and failed the docs' correct 11). - Docs: CHANGELOG round-5b section + MIGRATION behavior notes; stale token paths, a false "token is printed to the log" claim, the 30 s header deadline, a 10 s dispatch-timeout comment and db_health's per-table check corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
… log message Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
… CI) The loadend helper resolved false after a 2 s fallback. The requests go to the jsdom origin with no server, and on the Windows runner a refused localhost connection takes ~2 s to fail (TCP retries), so the fallback fired first and the test read an entry that was genuinely still in flight (then correctly aborted by the next open()). The fallback is 15 s and the test now asserts every request's loadend actually fired. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
…w the host answered On the Windows runner something answers the jsdom origin with 404 (locally nothing listens, so the request ends as `error`). The property under test is that re-open closes the first entry as `aborted` and the second reaches a final state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015A4eZdnsnQtTqs4DPRScaM
runyourempire
marked this pull request as ready for review
September 30, 2026 12:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Do not merge until the external adversarial audit is complete. This PR carries 0.9.0 release prep: the round-2 pre-audit red-team fixes plus the one-time
#[non_exhaustive]semver break. See CHANGELOG[0.9.0]and MIGRATION.md.Opened now so the full CI matrix (3 OSes, E2E, live-app proof, gauntlet, semver checks) runs against the release candidate. The semver-checks job reporting major changes is intended.
🤖 Generated with Claude Code
https://claude.ai/code/session_015Bpin9VXT6EoVDhoV8HJWw