Windows port (Wave 1-6): Zig 0.16 ABI fixes + std.c → std.Io migration#163
Open
lekt9 wants to merge 57 commits into
Open
Windows port (Wave 1-6): Zig 0.16 ABI fixes + std.c → std.Io migration#163lekt9 wants to merge 57 commits into
lekt9 wants to merge 57 commits into
Conversation
…/evaluate
1. HTTP read buffer in handleConnection increased from [8192]u8 to [65536]u8.
This prevents "CDP command failed" when eval results exceed 8KB — the read
buffer was the chokepoint for incoming HTTP requests including the CDP
response forwarded back to the caller.
2. /evaluate now reads expression from POST body first (raw text or JSON
{"expression":"..."}), falling back to ?expression= query param. This
allows the Node client to send large JS expressions (e.g. the 7.5KB
interceptor script) via POST instead of URL-encoding them into a query
string that exceeds OS/proxy URL length limits.
Fixes: unbrowse-ai/unbrowse-dev#408
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stealth & Anti-Bot: - Enhanced stealth.js: WebGL/canvas/AudioContext spoofing, chrome.csi stubs - Chrome 135 UAs, --disable-blink-features=AutomationControlled - Auto-stealth via Page.addScriptToEvaluateOnNewDocument on startup - KURI_PROXY env var for residential proxy support - --no-sandbox Linux-only (fixes justrach#128) - Successfully bypasses Singapore Airlines Akamai WAF Bot Block Detection: - /navigate auto-detects Akamai/Cloudflare/PerimeterX/DataDome blocks - Returns structured fallback with suggestions and proxy hints HAR Replay: - /har/replay endpoint: API map with curl/fetch/python code snippets - Captures full request headers and POST bodies from CDP events Security: - SSRF protection on /navigate via URL validation (fixes justrach#81) - JSON injection fix: escape all user content in JSON output (fixes justrach#82) CDP Stability: - EventBuffer use-after-free fix (fixes justrach#83) - 500 event headroom (was 100) for heavy SPAs - Auto-reconnect on WebSocket errors - Stale WebSocket cleanup in connectWs() Skills: - kuri-server skill for HTTP API browser automation - kuri-browse skill for terminal browsing Amp-Thread-ID: https://ampcode.com/threads/T-019d70d2-de3a-7398-b899-1d5061a8921d Co-authored-by: Amp <amp@ampcode.com>
…64→usize overflow guard (justrach#87) Amp-Thread-ID: https://ampcode.com/threads/T-019d70d2-de3a-7398-b899-1d5061a8921d Co-authored-by: Amp <amp@ampcode.com>
), setsockopt error (justrach#88), HTML entities (justrach#91) - extractHtmlValue: manual increment fixes adjacent escape skip bug - doHandshake: validates Upgrade header presence, not just HTTP 101 - setsockopt: returns ConnectionFailed instead of silently continuing - markdown: added 10 named HTML entities (rsquo, mdash, hellip, copy, etc.) - 8 new unit tests (250 total, all passing) Amp-Thread-ID: https://ampcode.com/threads/T-019d70d2-de3a-7398-b899-1d5061a8921d Co-authored-by: Amp <amp@ampcode.com>
…, HAR replay Amp-Thread-ID: https://ampcode.com/threads/T-019d70d2-de3a-7398-b899-1d5061a8921d Co-authored-by: Amp <amp@ampcode.com>
Breaking changes addressed: - std.net → raw C sockets (TcpStream/TcpServer in compat.zig) - std.time.timestamp/milliTimestamp/nanoTimestamp → clock_gettime shims - std.Thread.Mutex/RwLock/sleep → pthread shims - std.crypto.random → arc4random_buf - std.fs.File/cwd() → C open/read/write/mkdir/unlink - std.process.Child.init/run → fork/exec - std.process.argsAlloc → _NSGetArgc/_NSGetArgv - std.posix.getenv → std.c.getenv wrapper - ArrayList.writer() → direct .appendSlice/.print/.append - std.mem.trimRight/trimLeft → trimEnd/trimStart - std.heap.GeneralPurposeAllocator → DebugAllocator - std.http.Client needs io field - Compile.linkLibrary → root_module.linkLibrary - std.io.fixedBufferStream → removed New file: src/compat.zig — centralized 0.16 compatibility shims
This reverts commit d6a6adc.
This reverts commit ab14579.
release: prepare 0.3.1
Adds compact HTTP snapshots with a11y value/description/state and waits for tab/new hydration before the first session snapshot.
Use the release-channel branch for installers and future tag publishing instead of GitHub Releases.
EventBuffer.push stored the caller's allocator alongside the event. When the caller was a per-request arena that died after the request completed, a later flushEventsToHar would call item.owner.free(item.data) on a dead arena vtable — undefined behavior, manifesting as a SIGSEGV right after every "HAR flush: N buffered events" log line. Fix: dupe the event into the EventBuffer's own long-lived allocator at push time, free the original from the caller's arena while it is still alive, and store the buffer's own allocator as the owner. This decouples buffered-event lifetime from the caller's request scope. Repro: any browse session that buffers CDP events between requests (e.g. unbrowse go → close) reliably segfaulted on the next HAR flush. After this fix, the same chain runs cleanly with no SIGSEGVs.
Resolves 9 conflicts across 3 files by union-merging local features with
upstream API/style changes.
cdp/client.zig:
Take HEAD's EventBuffer dupe + early-free-on-failure (memory correctness).
chrome/launcher.zig:
Imports: keep both extensions_mod (local) + compat (upstream).
Launcher fields: keep state_dir + builtin_ext_path (local) + proxy (upstream).
Init defaults: same union.
findChromeBinary: take upstream's findExecutableCandidate(chrome_paths,
compat.getenv("PATH")) — both helpers exist in the auto-merged tree.
server/router.zig:
Read buffer: 65536 (local fix for POST body support) with upstream's
new init signature (stream, io, &buf).
handleEvaluate: upstream's requireEffectiveTabId for tab resolution +
HEAD's POST-body / JSON expression / query-param fallback chain.
Endpoint count test: HEAD's single-column format + /add-init-script
preserved (local feature).
NOTE: merged code requires Zig 0.16.0 (std.process.Init) — Lewis has 0.15.2
locally; build needs CI or local toolchain bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a sandboxed JS runtime for running anti-bot / signed-URL / HMAC bundles outside of Chrome. ~50ms cold start vs ~3s for full Chrome. The architectural insight: rather than reverse-engineer adversarial bundle math, be a faithful enough environment that the bundle fetches its own salts, runs its own computation, and hands us the cookie. The runtime IS the salt provider, by virtue of being a sufficient browser. Components: - src/sandbox/shim.js: ~30KB Web API shim (navigator, document, window, screen, location with URL polyfill, performance, crypto.subtle, fetch, XMLHttpRequest, localStorage, TextEncoder/Decoder, AudioContext + WebGL/canvas fingerprint stubs). - src/sandbox/runtime.zig: QuickJS wrapper, registers six native bridges (__nativeFetch, __nativeNowMs, __nativeRandomBytes, __nativeSubtleDigest, __cookieJarGet, __cookieJarSet). - src/sandbox/network.zig: outbound HTTP via curl-impersonate subprocess (fork+execvp, kuri's own pattern — std.process.spawn OOMs on 0.16 with the io vtable kuri uses). Body via tempfile @path. Response via --output tempfile to avoid stdout pipe collection. Cookie jar with Set-Cookie parsing and host-domain matching. - src/sandbox/fingerprint.zig: two builtin Chrome fingerprints (mac ARM, Windows), JSON serializer. - src/sandbox/handler.zig: POST /v1/sandbox/replay request shape + response builder. - src/sandbox_smoke.zig: 8 standalone tests (test-sandbox build step). Wired into src/server/router.zig as POST /v1/sandbox/replay. Tests: - zig build test-sandbox: 8/8 pass. - Real network round-trip via curl override: 654ms cold, post_eval returns parsed JSON from tls.peet.ws including spoofed UA. curl-impersonate is auto-discovered as `curl_<profile>` (e.g. curl_chrome131) on PATH, with $UNBROWSE_CURL_IMPERSONATE override for explicit binary paths. Plan: docs/deep-reveng.md (in unbrowse repo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace subprocess fork+execvp(curl) with statically-linked
libcurl-impersonate v1.5.6 (lexiforest fork). Single FFI call into a
self-contained 30MB archive (libcurl + BoringSSL + nghttp2 + brotli +
zstd + libpsl, all baked in). No PATH dependency, no env var, no fork
per request — Chrome 131 TLS handshake out of the box.
Result on tls.peet.ws fingerprint check:
Plain curl: JA4 t13d497h2_0d8feac7bc37_7395dae3b2f3 (no h2)
Kuri sandbox: JA4 t13d1516h2_8daaf6152771_02713d6af862 (h2)
↑ Chrome 131's actual JA4 fingerprint, with TLS 1.3
extensions and HTTP/2 negotiated.
Layout:
- vendor/curl-impersonate/<arch>-<os>/libcurl-impersonate.a
- vendor/curl-impersonate/include/curl/ (vanilla curl 8.10 headers)
- vendor/curl-impersonate/README.md (refresh procedure per platform)
- src/sandbox/curl_lib.zig (Zig FFI: easy_init, easy_setopt,
easy_impersonate, easy_perform,
write/header capture callbacks)
- src/sandbox/network.zig (driver: JSON headers → curl_lib,
Set-Cookie parsing into shared jar)
This commit ships aarch64-macos only (Lewis's dev box). Cross-platform
vendoring (x86_64-macos, aarch64-linux-gnu, x86_64-linux-gnu) is
mechanical — same .a per platform, build.zig already maps target.
Binary growth: 5.4MB → 9.8MB. Acceptable for "Kuri does adversarial
sites natively" positioning.
Build deps: BoringSSL is C++ → libc++ now linked. macOS frameworks
(CoreFoundation, Security, SystemConfiguration), libiconv, libicucore.
Linux: libidn2, libz, pthread, dl, libc++.
Tests: 9/9 sandbox-side pass (8 existing + 1 curl_easy_init smoke).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additions that complete the cookie pipeline:
1. Sandbox accepts seed_cookies in the POST /v1/sandbox/replay body.
Each cookie is re-encoded as a Set-Cookie line and fed through
applySetCookie so the same parsing/scoping rules apply as for
inbound responses. Populated BEFORE bundle eval so:
- document.cookie reads see them
- the bundle's outbound fetch calls send them via Cookie header
- Set-Cookie responses merge cleanly with the seeded state
Schema matches src/auth/browser-cookies.ts BrowserCookie shape:
{ name, value, domain, path, secure, http_only, same_site, expires }.
2. CURLOPT_ACCEPT_ENCODING="" tells libcurl to advertise all supported
encodings AND auto-decode the response. Without this we got gzip/br
compressed bodies (because curl_easy_impersonate sets
Accept-Encoding to Chrome's default "gzip, deflate, br, zstd"). The
bundle would then have to decompress in JS, which defeats the
purpose. One line, fixes everything.
End-to-end: with cookies extracted from Lewis's Dia session
(via findBestBrowserSession), Reddit's search.json now returns
authenticated JSON Listing — the same endpoint that returned
{"error":"Blocked"} to plain curl earlier in this thread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds x86_64-macos, aarch64-linux-gnu, x86_64-linux-gnu archives alongside the existing aarch64-macos. Each is a self-contained ~30-50MB static archive (libcurl + BoringSSL + nghttp2 + brotli + zstd + libpsl). build.zig already maps target → vendor dir. Native compile picks the right archive automatically. Cross-compile from macOS to non-native targets currently fails on system lib resolution (iconv, idn2 require target sysroot Zig doesn't ship); enable those once CI runners build them on the matching platform. Source: github.com/lexiforest/curl-impersonate v1.5.6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Windows vendored libs (12MB x86_64-windows + 11MB aarch64-windows).
Unlike Linux/macOS which ship a single self-contained
libcurl-impersonate.a, Windows release ships separate .lib files
(libcurl-impersonate, crypto, ssl, nghttp2, nghttp3, ngtcp2 + crypto,
brotli{common,dec,enc}, zlib, zstd) that need linking together with
Windows system libs (ws2_32, crypt32, secur32, bcrypt, etc.).
Server-side dep is solved. Kuri itself still needs POSIX→Windows port
work in chrome/launcher.zig (CreateProcessW instead of fork+execvp)
before Windows builds can be enabled. See docs/windows-port-plan.md
in the unbrowse repo for the full plan.
Source: github.com/lexiforest/curl-impersonate v1.5.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track every __nativeFetch call from sandbox bundles as a RouteRecord (url, method, status, final_url, content_type, body_excerpt, body_size, redirected). Surface them as routes_observed in /v1/sandbox/replay response. Caller (Node side) feeds these into extractEndpoints + marketplace publish so every authenticated agent fetch contributes to the discovery flywheel. Phase 1: Kuri-side tracking + JSON serialization (this commit). Phase 2: wire publishIndexedSkill on the unbrowse Node side (separate commit + /v1/skills/from-routes endpoint). body_excerpt capped at 4KB — enough for extractEndpoints to detect JSON shapes / API patterns without bloating the response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add optional `proxy` field to Request and thread it into the libcurl handle via CURLOPT_PROXY in perform(). libcurl parses user:pass from the URL itself, so a single setopt covers both unauthenticated and basic-auth proxies (HTTP, HTTPS, SOCKS5 — scheme dispatch is libcurl's job). Pairs with the unbrowse-side wire (SandboxReplayRequest.proxy → runBundleReplay JSON body → Kuri sandbox handler). Per-request override is required so different sites can route through different country-locked residential exits (geo.iproyal.com:_country-us etc). Falsifier in the unbrowse repo at tests/kuri-proxy-patch-shape.test.sh guards this commit from rebase drift (9 structural assertions + 4 adversarial mutations, all bite). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleEvaluate read request headers twice: once via requireEffectiveTabId before the body, and again via rememberCurrentTab AFTER readRequestBody. Zig 0.16 std.http.Server.Request.iterateHeaders() asserts the reader is still in .received_head state; readRequestBody() (readerExpectNone) advances it past that, so the second header access panicked with "reached unreachable code" (SIGABRT), killing the broker on every /evaluate request and cascading every client call into "Unable to connect". Resolve the session id once before the body read (arena-duped) and reuse the snapshot for setCurrentTab instead of re-reading headers. Static scan confirms handleEvaluate was the only handler with a post-body header read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Headless Chrome with no URL arg opens its privileged chrome://newtab/ NTP. kuri startup discovery registered it as a tab; it then lingered forever and became a SILENT wrong-tab fallback whenever a session tab_id could not be resolved (the observed "snap shows New Incognito tab" wedge under MCP transport churn). Append about:blank as the trailing Chrome positional arg so the startup tab is a benign, non-WebUI placeholder with no target-rotation and no misleading NTP surface. Verified: faithful in-process gate-flow repro now shows 0 chrome://newtab occurrences across HN/npm/lobste, 0 wedges.
getCdpClient(tab_id) connects a per-target /devtools/page/<id> socket. closeTarget issued Target.closeTarget over the targets OWN page session and discarded the response. Chrome tears that socket down as it closes the target, so the fire-and-forget command races teardown and is silently dropped: the Chrome tab survives while handleClose still removeTab()s it. Tabs accumulate across browse sessions and a later session->tab resolve binds to a stale leftover tab (observed in the MCP gate: probe 006 wikipedia session resolved live to probe 004s lobste.rs leftover tab; Chrome :9222 held 4 accumulated targets). Issue closeTarget over a SIBLING tab session (outlives the close) and confirm the response, mirroring how Target.createTarget reliably issues over a non-self session. Fall back to the targets own session only when it is the sole tab. Distinct from the NTP-startup (launcher about:blank) and drift-adopt fixes; this is the close()-never-actually-closes layer.
Headless Chrome renderer-backgrounds and timer-throttles non-foreground targets, so concurrent snap/eval across N tabs starved all but the active one (empty a11y tree). Adds the standard Playwright/Puppeteer flags: --disable-background-timer-throttling, --disable-backgrounding-occluded-windows, --disable-renderer-backgrounding, --disable-features=CalculateNativeWinOcclusion. Generic, not a per-site hack. Required for the parallel gate collector (.bench-gate falsifier). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ime alternatives GitHub Actions windows-latest native build (unbrowse-dev kuri-vendor.yml run 26162544394, job 76957890664) surfaced 12 Zig 0.16 compile errors when building kuri natively on Windows. Wave 1 closes 5 of those by gating the POSIX-only stdio + time syscalls behind comptime-known `is_windows` and routing through portable equivalents: - compat.zig L1-22 (Time): timestampSeconds / milliTimestamp / nanoTimestamp now use std.time.* on Windows (portable Zig stdlib); POSIX path unchanged. Removes clock_gettime references for the Windows target — clock_gettime's signature is incompatible with the x86_64_win calling convention in Zig 0.16's libc stub. - compat.zig writeToStdout/writeToStderr: GetStdHandle + kernel32.WriteFile on Windows; POSIX std.c.write unchanged. fd_t is *anyopaque on Windows, so `1` and `2` as comptime_int can't be passed to std.c.write — the kernel32 path takes a real HANDLE. - compat.zig stderrIsTty (new): GetFileType(handle) == FILE_TYPE_CHAR (0x02) on Windows; std.c.isatty unchanged on POSIX. - browse_main.zig L683: replace direct `std.c.isatty(2) != 0` with `compat.stderrIsTty()` so the same source compiles on both targets. This wave does NOT yet address the remaining 7 errors (kuri-fetch, merjs-e2e, kuri-agent compile failures + the fork/connect/pipe/dup2/socket sites in compat.zig L238-356). Those are larger redesigns and ship in Wave-2. Verify: - `zig build -Doptimize=ReleaseSafe` on macOS darwin-arm64 exits 0 (verified locally) — no regression to the working lane. - After push: trigger unbrowse-dev kuri-vendor.yml with `kuri_ref=feat/windows-port-wave-1`. Expect the windows-x64 row to advance past the 5 errors closed here and surface only the remaining 7 for the next wave. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave-1 used std.time.nanoTimestamp and std.os.windows.GetStdHandle, neither of which exists in Zig 0.16. Replace with explicit `extern "kernel32"` bindings in a comptime-gated `win` namespace so: - The bindings only exist when `is_windows == true`; POSIX builds get an empty struct. - `nanoTimestamp` uses QueryPerformanceCounter + QueryPerformanceFrequency, computed in i128 to avoid overflow. - `milliTimestamp` derives from `nanoTimestamp` for monotonicity consistency. - `writeToStdout`/`writeToStderr`/`stderrIsTty` use the manually-declared GetStdHandle/WriteFile/GetFileType (Zig 0.16's std.os.windows.kernel32 is nearly empty and doesn't ship those bindings). Still pending (next CI run will surface, Wave-2 ships): - compat.zig fork/pipe/dup2/connect/socket/read sites — need comptime-gated impl-struct pattern (the same shape used for the `win` namespace at the top) - agent_main.zig:1344 + fetch_main.zig:206 — direct std.c fd_t mismatches in caller files Verify: - `zig build -Doptimize=ReleaseSafe` on macOS darwin-arm64 exits 0 locally — Wave-1.1 doesn't regress the working lane. - Next CI re-run will surface only the remaining errors for Wave-2 to target. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave-1.1 surfaced the remaining 7 Zig 0.16 windows-x64 compile errors as POSIX-only sites in compat.zig + agent_main.zig + fetch_main.zig. Wave-2 gates each with `if (comptime is_windows)` early-return so the type-checker accepts the file on Windows while POSIX builds keep the real implementation. compat.zig - runCommand: @Panic on Windows (Wave-3 will ship CreateProcessW + pipe redirection). - isPortInUse: returns false on Windows (Wave-3: WSAStartup + WSASocket + connect). - TcpStream.{close,writeAll,read,write,setSockOpt}: NotImplementedOnWindows error / no-op. - tcpConnectToIp4, tcpListen, TcpServer.accept: NotImplementedOnWindows. - All `_ = x;` parameter discards removed — Zig 0.16 flags them as `pointless discard` because the function actually uses the param in the POSIX branch. agent_main.zig - fetchChromeTabs: returns NotImplementedOnWindows on Windows; POSIX path unchanged. Wave-3 routes via compat.tcpConnectToIp4 once that has a Windows impl. fetch_main.zig - L206 `std.c.isatty(2)` → `compat.stderrIsTty()`. Same pattern as browse_main.zig:683 from Wave-1. Verify: - `zig build -Doptimize=ReleaseSafe` on macOS darwin-arm64 exits 0 locally (verified) — no POSIX regression. - After push: kuri-vendor.yml windows-x64 row should advance past the type-checker. Runtime-on-Windows will panic on subprocess/socket use until Wave-3 ships the real impls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ding + gate cmdOpen/repl
Wave-2's `if (comptime is_windows) return X;` pattern doesn't comptime-fold POSIX std.c.clock_gettime / std.c.nanosleep extern decls — Zig still type-checks the un-taken branch and fails because timespec's parameter chain bottoms out at `void` on Windows. Switch to the proven impl-struct pattern (already used at the top of compat.zig for the `win` extern bindings).
compat.zig
- `time_impl = if (is_windows) struct { timestampSeconds/milliTimestamp/nanoTimestamp } else struct { ... POSIX ... }` — Zig instantiates only the chosen branch; the un-chosen struct body is never type-checked. Self-references inside the struct use `@This().milliTimestamp()` to avoid the ambiguity with the file-level wrappers.
- Same pattern for `thread_impl` (threadSleep): Sleep on Windows, std.c.nanosleep on POSIX.
- `win.Sleep` declared as `extern "kernel32" fn Sleep(DWORD) void`.
agent_main.zig
- `cmdOpen` Windows branch: early-return with fatal — fork+execvp is the wrong primitive on Windows. Wave-4+ will ship CreateProcessW with the same arg-marshalling.
browse_main.zig
- `repl` Windows branch: early-return — stdin fd_t differs on Windows (HANDLE vs c_int). Non-interactive flows unaffected.
Verify:
- `zig build -Doptimize=ReleaseSafe` on macOS darwin-arm64 exits 0 (verified) — POSIX path unchanged.
- After push: kuri-vendor.yml windows-x64 row should clear the 5 remaining errors (clock_gettime cascade, agent_main fork, browse_main fd_t). Likely surfaces vendored quickjs.zig "unused local constant" + any remaining direct std.c sites for the next wave.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p for Windows discoverTabs is the next file in the kuri-windows std.c-namespace-poison inventory after Wave-3. It uses raw std.c.write on a socket fd for the Chrome CDP /json/list HTTP request — same pattern as agent_main.zig fetchChromeTabs (gated Wave-2) and the compat.zig TcpStream methods (gated Wave-2). Early-return NotImplementedOnWindows on Windows; POSIX path untouched. Wave-4+ will ship compat.socketWrite (WSASend on Windows) so callers like this can stay portable. Verify: zig build -Doptimize=ReleaseSafe on darwin-arm64 exits 0 (verified locally). After push: kuri-vendor.yml windows-x64 row should clear one more error from the 3-remaining set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…indows Closes the std.c-namespace-poison inventory documented in the kuri-windows-port handoff. Each file's std.c-touching function gets an early-return on Windows via comptime gate. POSIX path unchanged. Per-file: - chrome/launcher.zig isExecutablePath: returns false on Windows (POSIX access/chmod/fork chain). - storage/local.zig deleteTreeAbsolute: noop on Windows (POSIX fork+exec for rm -rf). - storage/auth_profiles.zig listProfiles + deleteTreeAbsolute: empty result / noop (POSIX opendir+fork). - crawler/validator.zig posixPathIsSymlink: returns false on Windows (POSIX fstatat). - cdp/websocket.zig: module-level c_connect extern wrapped in posix_net comptime struct (only declared on POSIX); WebSocketClient.connect/close/writeAll early-return ConnectionFailed/WriteFailed on Windows. - agent_main.zig: connect extern wrapped same way + the call site uses posix_net.connect. After Wave-5, the only references to std.c.* in kuri source are inside compat.zig (already impl-struct-gated via Wave-3) and inside per-fn POSIX-only branches that are never reached on Windows. The kuri-vendor.yml windows-x64 row should clear all 3 remaining errors from Wave-3. Verify: zig build -Doptimize=ReleaseSafe on darwin-arm64 exits 0 (verified locally). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(Zig 0.16 Windows ABI fix)
Wave-5 closed leaf std.c usage but compat.zig itself called std.c.open at
cwdCreateFile/cwdReadFile/cwdWriteFile. The Windows ABI poison (`parameter
of type void not allowed in calling convention x86_64_win` in std/c.zig's
extern open() decl) propagated through every transitive caller —
kuri-vendor.yml run 26247053013 (2026-05-21) confirmed windows-x64 still
failed on `cwdWriteFile referenced from saveSession/extractBuiltinExtension`.
Wave-6 swaps to Zig 0.16's portable std.Io.Dir API:
pub fn cwdReadFile(allocator, path, max_size) ![]u8 {
var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();
return try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(max_size));
}
pub fn cwdWriteFile(path, data) !void {
var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = data });
}
std.Io.Dir.cwd() dispatches per-platform: kernel32 (WriteFile/ReadFile)
on Windows, posix syscalls on darwin/linux. No libc extern dependency,
no variadic-after-void parameter, no x86_64-win ABI issue.
cwdCreateFile (fd-returning helper) deleted — three call sites
(agent_main:793, fetch_main:160, storage/local:68) migrated from the
old `cwdCreateFile -> fdClose -> fdWriteAll` triplet to direct
`compat.cwdWriteFile(path, data)`. Collapses 3 lines of fd plumbing
into one portable call at each site.
API reference: confirmed via deepwiki ziglang/zig. Zig 0.16 deprecated
std.fs.cwd() in favor of std.Io.Dir.cwd() with required io: Io param
from std.Io.Threaded.init_single_threaded.io().
Local darwin-arm64 verify: `zig build -Dtarget=native-native` → green
(kuri, kuri-agent, kuri-fetch binaries built successfully).
Windows-x64 verify pending — push, bump unbrowse submodule SHA, fire
kuri-vendor.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add src/ffi.zig + a `zig build ffi` target that emits libkuri_ffi (.dylib/.so), exporting a stateless C-ABI fetch path: kuri_fetch(url, mode) -> NUL-terminated bytes (markdown|html), kuri_free, kuri_ffi_abi_version No server, no Bridge, no shared state — reuses validator.validateUrl (SSRF guard) + http_fetch.fetchHttp + markdown.htmlToMarkdown. Pure-Zig deps + spawned curl; needs no quickjs / curl-impersonate linkage. Lets a host (e.g. Bun bun:ffi dlopen) drive kuri's fetch/render in-process instead of running the long-lived kuri server. Verified: built native (aarch64-macos, 4.9MB .dylib); Bun FFI dlopen → kuri_fetch on https://example.com → markdown rendered in-process in ~2.5s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the Windows port of the main `kuri` broker exe and `kuri-agent`, which previously failed to cross-compile to x86_64-windows-gnu. Native darwin/linux behavior is unchanged (verified: Chrome launch + CDP + /health green on darwin-arm64). - chrome/launcher.zig: replace the POSIX fork()/execvp() Chrome spawn with the cross-platform Io process API (std.process.spawn → fork+exec on POSIX, CreateProcessW on Windows; stdio .ignore = /dev/null·NUL). Store the std.process.Child and kill()/wait() it portably. - compat.zig: Zig 0.16 removed std.Thread.Mutex/RwLock (now io-parameterized std.Io.Mutex) and Windows has no libc pthreads, so back PthreadMutex/RwLock with an atomic spin-mutex on Windows; keep the exact pthread ABI on POSIX. - cdp/client.zig, cdp/websocket.zig, server/router.zig: guard the remaining POSIX-only socket calls (setsockopt/read) behind comptime is_windows so they type-check; the CDP websocket transport stays a no-op on Windows pending a winsock port (connect() already returns ConnectionFailed there). - sandbox/curl_lib.zig: return NotImplementedOnWindows before any curl extern so libcurl-impersonate symbols stay unreferenced (the vendored Windows archives are MSVC-ABI, incompatible with the -gnu cross-target); the sandbox falls back to subprocess curl as designed. CDP browse does not use this path. - storage/auth_profiles.zig: fix the empty-slice return literal on Windows.
…e on Windows) The CDP websocket client (src/cdp/websocket.zig) was POSIX-only — connect() returned ConnectionFailed on Windows, so kuri.exe could link and serve /health but not talk to Chrome. Implement the transport over winsock (ws2_32): - websocket.zig: add minimal ws2_32 externs (WSAStartup/socket/connect/send/ recv/setsockopt/closesocket) — Zig 0.16's std omits these. connect() now opens a real winsock socket on Windows (POSIX path unchanged); writeAll/rawRead/close use send/recv/closesocket. The SOCKET handle is stashed in the existing `fd: std.posix.fd_t` field via int↔ptr casts, so the struct shape is identical across platforms. Windows SO_RCVTIMEO is a DWORD-millis, handled accordingly. - build.zig: link ws2_32 for every Windows binary that pulls in the CDP path. Verified: kuri.exe cross-compiles with winsock linked (PE32+); native darwin CDP-websocket path still connects to real Chrome (kuri launches Chrome, /health reports a discovered tab) — the shared handshake/frame logic + refactored connect() are intact. Live Windows browse runtime is CI-gated (test-windows.yml). Next: discoverTabs (HTTP /json/list) winsock port for Windows startup tab discovery; currently NotImplementedOnWindows (CDP target discovery still works over the websocket).
…TA%) kuri.exe started and served /health on Windows but failed at launchChrome: "no Chrome binary found". chrome_paths had only macOS + Linux names, and isExecutablePath short-circuited to false on Windows (it's a POSIX access(X_OK) probe). Fix both: - chrome_paths: add a .windows arm (Program Files / Program Files (x86) / Chromium standard install locations). - isExecutablePath: on Windows, probe existence via compat.cwdAccess (mingw access(F_OK)) — there is no exec bit; an .exe is runnable if it exists. - findChromeBinary: also check the per-user %LOCALAPPDATA%\Google\Chrome install (a runtime path, not a compile-time constant), into a file-scoped buffer so the returned slice stays valid. Verified: kuri.exe cross-compiles (PE32+); native darwin Chrome discovery + launch unchanged (/health → tabs:1). Surfaced by the windows-latest E2E run, which got kuri.exe to start + listen but could not find Chrome to launch.
On Windows kuri.exe found + launched Chrome but hung in startup: waitForDebuggerUrl
loops on httpProbe → compat.tcpConnectToIp4/TcpStream, which were stubbed
NotImplementedOnWindows. So it never read Chrome's /json/version, never got the
websocket debugger URL, and exited before serving /health.
Implement winsock (ws2_32) in compat.TcpStream.{close,read,write,writeAll} and
tcpConnectToIp4 — the shared loopback-TCP primitive used by the CDP HTTP probes
(and discoverTabs' sibling paths). Mirrors the websocket.zig winsock client;
SOCKET stashed in the fd_t field via int↔ptr. ws2_32 already linked in build.zig.
POSIX path unchanged.
Verified: kuri.exe cross-compiles (PE32+); native darwin startup + /health
unchanged (tabs:1). Surfaced by windows-latest: "launched Chrome on CDP port
9222" then silence — the probe to read the debugger URL was the gap.
…completes On Windows kuri.exe got Chrome's CDP endpoint (winsock probe works) but then aborted: main.zig does `try discoverTabs(...)` and discoverTabs returned NotImplementedOnWindows (it used std.Io.net + raw std.c.write/std.posix.read). Rewrite discoverTabs' GET /json/list over compat.tcpConnectToIp4/TcpStream — the now-winsock-capable cross-platform loopback TCP primitive (same one httpProbe uses). Removes the Windows stub; POSIX behavior unchanged. Verified: kuri.exe cross-compiles (PE32+); native darwin "startup discovery registered 1 tabs" + /health tabs:1 unchanged. Surfaced by windows-latest: "CDP endpoint: ws://..." then NotImplementedOnWindows at router.zig discoverTabs.
…ng logins
Visible-mode Chrome launched with one shared `$HOME/.kuri/chrome-profile`. With
concurrent sessions (each on its own CDP port — e.g. interactive logins across
parallel browse sessions), Chrome's SingletonLock + cookie-DB clobber-on-close
made one session overwrite another's login: the "logins constantly purged /
logged out" symptom, and a gap in per-session isolation.
Use a per-port profile (`chrome-profile-{cdp_port}`) so concurrent instances are
isolated; cross-session cookie persistence stays the auth-profile vault's job
(authProfileLoad on browse_go re-injects each domain's cookies). Compile-verified
(zig build; the per-port string is in the kuri exe).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ogin-purge) Root-caused from live vault state: saveProfile writes the keychain entry first (keychainUpsert) and the meta file second (writeMetaFile). If the meta write fails or is later lost, the cookies are orphaned — they sit in the keychain but loadProfile hard-required `readMetaFile` and threw, so auth was never restored and the user appeared logged out though the cookies were right there. Confirmed on a real machine: github.com + figma.com had keychain entries but NO meta file (310 keychain entries vs 5 meta files). loadProfile now falls back to a direct keychain read by the original name when the meta is missing (default keychain backend), recovering the orphaned cookies. No-op on the file backend. Compile-verified (zig build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
49 commits porting Kuri to Windows on Zig 0.16:
fd_ttyping for stdio + clock_gettime alternativescmdOpen/repldiscoverTabsstd.c.writesocket loopstd.c-touching filescwd*Filefromstd.c.opentostd.Io.DirPlus parallel-multi-tab tabbed-throttling disable,
closeTargetover sibling sessions, andchrome://newtab→about:blankfor initial tab.Test plan
kuri.exebuilds with Zig 0.16 + serves CDPkuri newtab/setCookie/getCurrentUrlend-to-endThis is the upstream-merge step the unbrowse repo's submodule has been pinned on. Once this merges, the unbrowse
.gitmodules(which already points at this repo @adding-extensions) will resolve cleanly from a fresh clone with the Windows port included.🤖 Generated with Claude Code