Skip to content

feat(windows): enable MCP mode and telemetry on Windows#172

Open
JF10R wants to merge 8 commits intojustrach:mainfrom
JF10R:windows/mcp-telemetry
Open

feat(windows): enable MCP mode and telemetry on Windows#172
JF10R wants to merge 8 commits intojustrach:mainfrom
JF10R:windows/mcp-telemetry

Conversation

@JF10R
Copy link
Copy Markdown

@JF10R JF10R commented Apr 6, 2026

Closes #183
Depends on #170, #171

Summary

Enables MCP mode and telemetry on Windows: HOME/USERPROFILE fallback, LLVM frame merging prevention, cross-platform telemetry sync, and blocking the unsupported HTTP serve command.

Changes

File Change
src/main.zig is_windows const; block serve command with error directing to mcp mode
src/mcp.zig HOMEUSERPROFILE fallback in getDataDir, handleProjects, ProjectCache; noinline/never_inline for heavy MCP handlers; compat.path_buf_size for path buffers; pass cwd to Child.run in handleIndex
src/telemetry.zig Argv-based curl execution (no shell interpolation — fixes command injection); compat.path_buf_size; spawnAndWait instead of orphaned spawn
src/snapshot.zig HOMEUSERPROFILE fallback for secondary snapshot path

Tests

zig build test — 252/258 passed, 2 failed (pre-existing), 4 skipped (POSIX-only)

Before/After

  • Before: MCP mode crashes on startup — HOME env var doesn't exist on Windows. LLVM inlines all handler frames into ~128MB. Telemetry uses /bin/sh. serve command hits POSIX-only std.net.
  • After: MCP mode starts and responds to tool calls. Telemetry syncs via direct curl argv. serve blocked with clear error message.

Compliance

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fa8ff4089

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +176 to +177
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H 'Content-Type: application/json' --data-binary @{s} >/dev/null 2>&1 && : > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "/bin/sh", "-c", cmd }, std.heap.page_allocator);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid shell interpolation for telemetry upload path

Building a /bin/sh -c command with {s}-interpolated path reintroduces command injection and command-breakage risks that the previous argv-based curl call avoided. path is derived from environment/root-derived values (HOME/USERPROFILE and project location), so shell metacharacters or spaces in those paths can execute unintended commands or make telemetry sync fail in production; this should pass arguments directly via Child.init argv (without a shell) as before.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables Windows support for codedb’s MCP mode and telemetry by reducing stack pressure, adding Windows-specific environment/path handling, and gating unsupported HTTP server mode on Windows.

Changes:

  • Introduce compat.path_buf_size and apply it to stack-allocated path buffers across watcher/MCP/telemetry/main.
  • Heap-allocate large data structures to avoid Windows stack overflows (watcher EventQueue, index frequency-table buffers) and add noinline/never_inline barriers for MCP handlers.
  • Add HOMEUSERPROFILE fallback in multiple places and block serve on Windows with a guided error.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/compat.zig Adds path_buf_size to cap stack path buffers on Windows.
src/watcher.zig Switches FsEvent buffers to path_buf_size; heap-allocates EventQueue with init/deinit.
src/tests.zig Updates watcher queue tests to use EventQueue.init/deinit.
src/index.zig Heap-allocates frequency-table buffers; refactors finishFrequencyTable signature.
src/main.zig Adds is_windows, blocks serve on Windows, uses path_buf_size, updates watcher queue ownership.
src/mcp.zig Uses path_buf_size, adds HOME/USERPROFILE fallback, adds inlining controls, sets cwd for indexing child process.
src/telemetry.zig Uses path_buf_size; changes cloud sync execution strategy for Windows/POSIX.
src/snapshot.zig Adds HOMEUSERPROFILE fallback for secondary snapshot path.
build.zig Sets Windows executable stack size to 8MB.

Comment on lines +165 to +182
if (comptime @import("builtin").os.tag == .windows) {
// On Windows, use cmd.exe to run curl and truncate the file
var cmd_buf: [2048]u8 = undefined;
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H \"Content-Type: application/json\" --data-binary @{s} >NUL 2>&1 && type nul > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "cmd.exe", "/C", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
} else {
var cmd_buf: [2048]u8 = undefined;
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H 'Content-Type: application/json' --data-binary @{s} >/dev/null 2>&1 && : > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "/bin/sh", "-c", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syncToCloud now constructs a shell command string (both cmd.exe /C on Windows and /bin/sh -c elsewhere) that interpolates path directly without robust quoting/escaping. This reintroduces command-injection risk (regression from the previous argv-based Child.init(&.{"curl", ...})) and will also break for valid telemetry paths containing spaces or shell metacharacters. Prefer spawning curl with an argv array (no shell) and truncating the file via Zig after a successful exit, or otherwise ensure path is correctly quoted/escaped for the target shell.

Suggested change
if (comptime @import("builtin").os.tag == .windows) {
// On Windows, use cmd.exe to run curl and truncate the file
var cmd_buf: [2048]u8 = undefined;
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H \"Content-Type: application/json\" --data-binary @{s} >NUL 2>&1 && type nul > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "cmd.exe", "/C", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
} else {
var cmd_buf: [2048]u8 = undefined;
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H 'Content-Type: application/json' --data-binary @{s} >/dev/null 2>&1 && : > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "/bin/sh", "-c", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
}
var data_arg_buf: [1024]u8 = undefined;
const data_arg = std.fmt.bufPrint(&data_arg_buf, "@{s}", .{path}) catch return;
var child = std.process.Child.init(&.{
"curl",
"-sf",
"-X",
"POST",
CLOUD_URL,
"-H",
"Content-Type: application/json",
"--data-binary",
data_arg,
}, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
child.spawn() catch return;
const term = child.wait() catch return;
switch (term) {
.Exited => |code| {
if (code != 0) return;
},
else => return,
}
var file = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch return;
defer file.close();
file.setEndPos(0) catch return;

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +181
var child = std.process.Child.init(&.{ "cmd.exe", "/C", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
} else {
var cmd_buf: [2048]u8 = undefined;
const cmd = std.fmt.bufPrint(&cmd_buf, "curl -sf -X POST {s} -H 'Content-Type: application/json' --data-binary @{s} >/dev/null 2>&1 && : > {s}", .{ CLOUD_URL, path, path }) catch return;
var child = std.process.Child.init(&.{ "/bin/sh", "-c", cmd }, std.heap.page_allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Ignore;
child.stderr_behavior = .Ignore;
_ = child.spawn() catch return;
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syncToCloud uses child.spawn() and then drops the Child without waiting. On POSIX this will leave zombie processes until codedb exits, and with syncToCloud() called every 10 events this can exhaust the process table over long sessions. Consider using spawnAndWait(), or spawning in a background thread and still calling wait() to reap, or otherwise retaining/reaping child processes.

Copilot uses AI. Check for mistakes.
@JF10R JF10R force-pushed the windows/mcp-telemetry branch 2 times, most recently from d094955 to a81c763 Compare April 6, 2026 15:05
@JF10R
Copy link
Copy Markdown
Author

JF10R commented Apr 6, 2026

Addressing reviewer feedback

✅ Fixed: telemetry.zig:177 — shell injection via string interpolation (P1)

Reverted to upstream's argv-based curl execution. No shell involved — arguments are passed directly to execve/CreateProcess, eliminating command injection risk from paths containing spaces, quotes, or metacharacters. (commit a81c763)

✅ Fixed: telemetry.zig:181 — zombie processes from spawn() without wait

Switched back to spawnAndWait() (matches upstream). The telemetry sync is not on a hot path — it runs periodically and blocking briefly for curl (max 5s timeout) is acceptable. No more orphaned child processes.

✅ Fixed: telemetry.zig:169 — same injection concern

The entire shell-interpolation approach was replaced. Both Windows and POSIX now use the same argv-based curl invocation. curl ships with Windows 10+ so no cmd.exe wrapper is needed. File truncation after sync uses createFile(.truncate = true) which is cross-platform.

@justrach
Copy link
Copy Markdown
Owner

justrach commented Apr 6, 2026

Hey @JF10R — thanks for the Windows work! Before we can review and merge, could you please:

  1. Rebase onto current main — we've landed several changes (mmap trigram index, fuzzy search, process lifecycle fixes, root policy updates) that may conflict
  2. Follow the requirements in CONTRIBUTING.md — each PR needs:
    • Linked issue number
    • Summary of exact changes + files touched
    • Tests run (include zig build test output)
    • Failing test before fix / passing test after
    • Confirmation branch is rebased onto current main
    • No generated artifacts committed
  3. Keep PRs tightly scoped — the foundation → I/O → MCP → security chain looks good, just make sure each one compiles independently after rebase

We'll get Codex to do a review pass once rebased. Thanks!

Copy link
Copy Markdown
Owner

@justrach justrach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting rebase onto current main and CONTRIBUTING.md compliance before review. See comment above.

JF10R added 8 commits April 6, 2026 19:37
Add Windows platform support foundations that all subsequent Windows PRs
depend on. No behavioral change on Linux/macOS.

- compat.zig: add path_buf_size (1024 on Windows vs max_path_bytes)
- build.zig: set 8MB stack for Windows (matches Linux default)
- main.zig: replace thread trampoline with noinline mainImpl to avoid
  LLVM frame merging (~128MB) that exceeds Windows guard page stride
- main.zig: fix double scan_thread.join() (UB on all platforms)
- watcher.zig: heap-allocate EventQueue (avoids ~4MB stack array)
- watcher.zig: use compat.path_buf_size in FsEvent
- index.zig: heap-allocate frequency table buffers (~640KB each)
- tests.zig: update EventQueue construction to use init()/deinit()
Upstream issue-148 added stdin HUP detection via std.posix.poll() and
pipe tests. These use POSIX APIs unavailable on Windows. Guard the poll
block with comptime os check (Windows uses idle timeout only) and skip
pipe tests on Windows.
Upstream's mmap trigram index (justrach#167) uses std.posix.mmap/munmap which
are unavailable on Windows. Guard initFromDisk to return null on Windows
(falling back to heap-based index) and skip mmap-dependent tests.
Platform-specific I/O changes for Windows file system operations.

- store.zig: use NtLockFile/NtUnlockFile on Windows (POSIX flock on others)
- watcher.zig: add isSep() to handle both / and \ path separators
- watcher.zig: backslash-aware shouldSkip() for Windows paths
- watcher.zig: use %TEMP%/%TMP% for codedb-notify path on Windows
- watcher.zig: use setEndPos() instead of ftruncate (cross-platform)
- watcher.zig: trim both / and \ when making paths relative to root
- store.zig: check NtLockFile return status; only unlock if lock
  succeeded; skip write-under-lock if lock fails (matches upstream's
  advisory lock pattern)
- watcher.zig: normalize backslash paths to forward slashes in
  drainNotifyFile to avoid duplicate entries in the explorer (walker
  uses / convention, notify file may contain \ on Windows)
Remove barriers preventing MCP and telemetry from working on Windows.

- main.zig: add is_windows const; block HTTP serve with error message
  directing users to mcp mode; HOME→USERPROFILE fallback in getDataDir
- mcp.zig: import compat; noinline on run/dispatch to prevent LLVM frame
  merging; never_inline for heavy handlers (handleRemote, handleIndex,
  handleProjects); HOME→USERPROFILE fallback; path_buf_size; pass cwd
  to Child.run in handleIndex
- snapshot.zig: HOME→USERPROFILE fallback in writeSnapshotDual
- telemetry.zig: use cmd.exe /C for cloud sync on Windows; use
  compat.path_buf_size for buffer declarations
Revert from shell command string interpolation (cmd.exe /C, /bin/sh -c)
to argv-based curl execution. This:
- Eliminates command injection risk from path interpolation
- Avoids zombie processes (spawnAndWait instead of spawn)
- Works on both Windows (curl ships with Win10+) and POSIX
- Truncates telemetry file after successful sync via createFile
@JF10R JF10R force-pushed the windows/mcp-telemetry branch from a81c763 to bde3bf7 Compare April 6, 2026 23:45
@JF10R
Copy link
Copy Markdown
Author

JF10R commented Apr 6, 2026

Rebased onto current main (d7e3bfb), updated PR description per CONTRIBUTING.md.

Resolved rebase conflict in mcp.zig dispatch: merged upstream codedb_find handler with our never_inline wrappers.

zig build test output

$ zig build test
Build Summary: 5/7 steps succeeded; 1 failed; 297/303 tests passed; 4 skipped; 2 failed
  run test 252/258 passed, 2 failed, 4 skipped

Same pass rate as #170/#171 — no regressions introduced.

2 failed (pre-existing — see #170 comment for proof):

  • issue-150: --help prints usage
  • issue-150: -h prints usage

4 skipped: same POSIX-only tests as #170.

Red-to-green

Before (without this PR, on Windows):

  • MCP startup crashes: std.process.getEnvVarOwned("HOME") → error (Windows uses USERPROFILE)
  • Telemetry sync: /bin/sh -c curl ... → file not found
  • serve command: POSIX-only std.net server code → compilation/runtime failure

After:

  • HOMEUSERPROFILE fallback in getDataDir, handleProjects, ProjectCache, writeSnapshotDual
  • Telemetry uses argv-based curl via spawnAndWait (no shell, no zombies)
  • serve blocked with error: "HTTP serve mode is not supported on Windows. Use: codedb mcp"
  • MCP starts and responds to all tool calls on Windows

Nearby non-regression checks

All 252 passing tests are unchanged upstream tests. Specifically:

  • All MCP handler tests pass (dispatch, JSON parsing, tool responses)
  • All telemetry tests pass (event recording, flush cadence)
  • All snapshot tests pass (dual-path write with HOME/USERPROFILE)
  • issue-148: idle timeout is 10 minutes passes (upstream timeout preserved)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Windows support — MCP mode and telemetry

3 participants