Skip to content

feat: local-server trial — restore HTTP port, configurable CODEDB_PORT, O(1) findSymbol, MCP stdout fix#310

Merged
justrach merged 9 commits intomainfrom
feat/local-server-trial
Apr 30, 2026
Merged

feat: local-server trial — restore HTTP port, configurable CODEDB_PORT, O(1) findSymbol, MCP stdout fix#310
justrach merged 9 commits intomainfrom
feat/local-server-trial

Conversation

@justrach
Copy link
Copy Markdown
Owner

@justrach justrach commented Apr 22, 2026

Summary

Trial branch gathering the local-server work that was sitting on the working tree, plus a fix for #304 (MCP stdio stdout contamination) and the three Codex review items.

Commits, each scoped to one change:

  1. c4cc763 Restore codedb serve --port on Zig 0.16 (src/server.zig) — ports the legacy HTTP endpoint (stubbed in 56ea465 / v0.2.578) to the new std.Io.net surface. Routes and JSON shapes match the pre-0.16 build. Refs Restore codedb serve --port HTTP endpoint on Zig 0.16 #307, chore(0.16): MCP + server — mcp.zig, server.zig + Child.run #285.
  2. 2fbc66c Route MCP status output to stderr (src/main.zig) — switches out.file to stderr once cmd == "mcp". Closes MCP stdio: failure-path status text is written to stdout and corrupts JSON-RPC #304.
  3. aaba92e / 14c3160 / 8b43e89 codedb serve port handling (src/main.zig) — lands at: default port 6767, CODEDB_PORT as optional override. codedb serve is its own opt-in; no separate gate. Refs Make codedb serve port configurable via CODEDB_PORT env var #308.
  4. c9d773c / fbb8b49 O(1) findAllSymbols (src/explore.zig) — index lookup for the common case, with a merging outline scan (deduped on (path, line_start)) so fast-snapshot restore can't drop untouched files. rebuildSymbolIndexFor no longer skips .import / .comment_block. Refs findSymbol should use symbol_index for O(1) lookup instead of scanning all outlines #309, addresses Codex P1.
  5. 74ba881 Harden server.isPathSafe (src/server.zig) — reject null bytes and \\ so Windows-style ..\\..\\x can't bypass the /-split .. check. Mirrors mcp.isPathSafe. Addresses Codex P1.
  6. 6ef7185 /file/read resolves against indexed root (src/server.zig) — opens via explorer.root_dir instead of std.Io.Dir.cwd(), so launching codedb /some/root serve from elsewhere no longer false-404s or reads the wrong file. Addresses Codex P2.

Test plan

  • zig build clean on macOS arm64, Zig 0.16.0
  • codedb /private/tmp mcp — stdout: 0 bytes, stderr: ✗ refusing to index temporary root: /private/tmp
  • codedb --mcp </dev/null — stdout: 0 bytes, stderr: info: codedb mcp: root=…
  • codedb serve — binds 127.0.0.1:6767; curl :6767/ → JSON 404
  • CODEDB_PORT=12345 codedb serve — binds 127.0.0.1:12345
  • curl 'http://127.0.0.1:6767/file/read?path=..\\..\\etc\\passwd'403 {"error":"path traversal not allowed"}
  • curl 'http://127.0.0.1:6767/file/read?path=src/main.zig' → 200 (resolved under the indexed root, not cwd)
  • codedb find Explorer still returns import hits (index + scan both covered)
  • Micro-bench findAllSymbols on a large project to confirm the O(1) win survives the dedup scan

Notes

🤖 Generated with Claude Code

justrach and others added 4 commits April 22, 2026 10:41
Port the legacy HTTP endpoint (stubbed in 56ea465 / v0.2.578) to the new
`std.Io.net` surface: bind 127.0.0.1 with `IpAddress.parse`/`listen`,
accept in a loop, and hand each stream to a detached thread. The routes
and JSON response shapes match the pre-0.16 implementation so existing
clients don't need changes.

Refs #307, #285

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For `codedb mcp`, stdout is reserved for JSON-RPC messages. The root-policy
failure path wrote `✗ refusing to index temporary root: …` (and the normal
`✓ indexed` startup line) to stdout, which hosts reject with
`invalid character 'â' looking for beginning of value` on the leading UTF-8
byte of the status glyph.

Switch `out.file` to stderr once `cmd == "mcp"` is resolved, so every `out.p`
call on that path goes to stderr while stdout stays clean for protocol
messages.

Closes #304

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read `CODEDB_PORT` from the environment; fall back to 7719 on absence
or parse failure. Unblocks running multiple instances on one host,
reverse-proxy setups, and integration tests that need an ephemeral port.

Refs #308

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`Explorer.findSymbol` now looks up the name in `self.symbol_index` and
builds results from the cached locations. The full outline scan is kept
as a fallback for safety.

For the index to be authoritative, `rebuildSymbolIndexFor` no longer
skips `.import` / `.comment_block` kinds — those were being missed by the
O(1) path and forced callers into the slow scan. Indexing every kind
makes results match the scan-based path exactly.

Refs #309

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Benchmark Regression Report

Threshold: 10.00%

Tool Base (ns) Head (ns) Delta Status
codedb_bundle 490368 484436 -1.21% OK
codedb_changes 54303 53900 -0.74% OK
codedb_deps 9343 8997 -3.70% OK
codedb_edit 6140 4950 -19.38% OK
codedb_find 59160 60049 +1.50% OK
codedb_hot 97826 97581 -0.25% OK
codedb_outline 237436 240104 +1.12% OK
codedb_read 84415 81578 -3.36% OK
codedb_search 174815 171955 -1.64% OK
codedb_snapshot 2494756 2570053 +3.02% OK
codedb_status 213781 213691 -0.04% OK
codedb_symbol 55567 51962 -6.49% OK
codedb_tree 74737 73342 -1.87% OK
codedb_word 69388 67689 -2.45% OK

Drop the hardcoded 7719 fallback. If CODEDB_PORT is unset, `codedb serve`
exits with a clear message explaining how to enable it (suggested 47719,
since 7719 and 8080 tend to collide with other local processes). If set
but not parseable as u16, exit with an error.

Rationale: the HTTP server opens a network port; having it bind on a
predictable default when someone runs `codedb serve` accidentally is
worth avoiding. Treating the env var as the on/off switch keeps the
surface area minimal and makes the enabled case explicit in shell
history / process listings.

Refs #308

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Benchmark Regression Report

Threshold: 10.00%

Tool Base (ns) Head (ns) Delta Status
codedb_bundle 316864 300141 -5.28% OK
codedb_changes 32291 30917 -4.26% OK
codedb_deps 5793 4748 -18.04% OK
codedb_edit 4542 4112 -9.47% OK
codedb_find 45049 41548 -7.77% OK
codedb_hot 59448 57279 -3.65% OK
codedb_outline 157625 150308 -4.64% OK
codedb_read 56759 57547 +1.39% OK
codedb_search 143501 152510 +6.28% OK
codedb_snapshot 2002325 1996873 -0.27% OK
codedb_status 243591 233139 -4.29% OK
codedb_symbol 33942 28703 -15.44% OK
codedb_tree 46525 46933 +0.88% OK
codedb_word 42110 41457 -1.55% OK

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: c9d773c9ca

ℹ️ 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 thread src/server.zig
Comment on lines +699 to +702
var it = std.mem.splitScalar(u8, path, '/');
while (it.next()) |component| {
if (std.mem.eql(u8, component, "..")) return false;
}
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 Block Windows-style separators in path safety checks

server.isPathSafe only rejects absolute / paths and .. segments split on '/', so inputs like ..\\..\\secret.txt are accepted. On Windows, \ is a real path separator, so this bypass can reach files outside the intended tree through /file/read and /edit. src/mcp.zig:isPathSafe already rejects backslashes; the HTTP server should enforce the same rule to avoid traversal gaps.

Useful? React with 👍 / 👎.

Comment thread src/explore.zig
Comment on lines +1260 to +1263
if (self.symbol_index.get(name)) |locs| {
for (locs.items) |loc| {
var detail: ?[]const u8 = null;
if (self.outlines.getPtr(loc.path)) |outline| {
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 Preserve fallback scan when symbol index is incomplete

This new early return assumes symbol_index is authoritative, but that is not always true after fast snapshot restore: restored files are inserted into outlines without rebuilding symbol_index, and later watcher/edit updates repopulate the map only for touched files. In that mixed state, symbols present in untouched files are silently omitted because the outline scan fallback never runs once get(name) is non-null. Keep scanning for missing hits or rebuild the full symbol index after fast restore.

Useful? React with 👍 / 👎.

Comment thread src/server.zig Outdated
respondJson(&conn, "403 Forbidden", "{\"error\":\"path traversal not allowed\"}");
return;
}
const content = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(10 * 1024 * 1024)) catch |err| switch (err) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve file reads against configured project root

/file/read currently uses std.Io.Dir.cwd() to open files, but codedb <root> serve indexes paths relative to the provided root. If the process is launched from another directory, valid indexed paths are resolved against the wrong base (causing false 404s or reading the wrong file). The read handler should use the same root directory context used by indexing/watching.

Useful? React with 👍 / 👎.

justrach and others added 4 commits April 22, 2026 10:55
Running `codedb serve` is itself the opt-in — codedb has no always-on
daemon, so gating the listener behind an additional CODEDB_PORT
requirement was belt-and-suspenders with no threat to block. Restore the
previous UX: `codedb serve` starts listening on a default port,
CODEDB_PORT stays as an optional override for collisions.

Default is now 6767 (picked off the beaten path — 7719 and 8080
collided with other local tooling).

Refs #308

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`isPathSafe` only rejected absolute `/` paths and `..` segments split on
`/`, so inputs like `..\\..\\secret.txt` passed through — on platforms
where `\\` is a real separator this could reach files outside the
indexed tree through `/file/read` and `/edit`. Null bytes likewise could
truncate paths in downstream syscalls.

Mirror `mcp.isPathSafe`: reject null bytes and backslashes up front
before the `/`-split loop.

Addresses Codex P1 on #310.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The HTTP handler was opening files via `std.Io.Dir.cwd()`, but `codedb
<root> serve` indexes paths relative to the provided root. Launched from
any other directory, valid indexed paths hit the wrong base and returned
false 404s (or worse — read the wrong file).

Open via `explorer.root_dir` instead. Respond 500 with a clear error if
the root was never configured (shouldn't happen on the normal serve
path, but guards against a bare explorer).

Addresses Codex P2 on #310.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The earlier change early-returned after the `symbol_index` lookup, but
that map can be incomplete after fast-snapshot restore — `outlines` is
populated before `rebuildSymbolIndexFor` runs on every file, and later
watcher/edit updates only touch files they saw change. Symbols present
in untouched files were silently dropped from results once the index
had any entry for the name.

Keep the O(1) path for the common case, but always fall through into
the outline scan and dedupe against a per-call `(path, line_start)` set
so the scan fills gaps without duplicating index hits.

Addresses Codex P1 on #310.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Benchmark Regression Report

Threshold: 10.00%

Tool Base (ns) Head (ns) Delta Status
codedb_bundle 490185 487154 -0.62% OK
codedb_changes 55140 60336 +9.42% OK
codedb_deps 8739 9396 +7.52% OK
codedb_edit 6136 6193 +0.93% OK
codedb_find 63437 61175 -3.57% OK
codedb_hot 97226 103922 +6.89% OK
codedb_outline 239213 247009 +3.26% OK
codedb_read 88123 86092 -2.30% OK
codedb_search 178565 179418 +0.48% OK
codedb_snapshot 2520420 2653844 +5.29% OK
codedb_status 215572 216091 +0.24% OK
codedb_symbol 57533 59712 +3.79% OK
codedb_tree 76026 77778 +2.30% OK
codedb_word 67824 69103 +1.89% OK

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: fbb8b49b4c

ℹ️ 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 thread src/server.zig
}
if (after) |a| req.after = @intCast(a);

const result = edit_mod.applyEdit(io, allocator, store, agents, explorer, req) catch |err| {
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 Resolve /edit paths against the configured project root

The HTTP /edit handler forwards path to edit_mod.applyEdit, but that helper performs file I/O via std.Io.Dir.cwd() (in src/edit.zig) rather than the indexed root directory. In codedb <root> serve sessions started from another working directory, edits can fail with FileNotFound or modify a same-named file under the process CWD instead of <root>, which can corrupt unrelated files.

Useful? React with 👍 / 👎.

Comment thread src/server.zig
Comment on lines +291 to +294
if (!isPathSafe(path)) {
respondJson(&conn, "403 Forbidden", "{\"error\":\"path traversal not allowed\"}");
return;
}
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 Block sensitive paths in HTTP edit/read endpoints

The HTTP handlers only apply traversal checks and do not enforce watcher.isSensitivePath(path), unlike MCP read/edit handlers in src/mcp.zig. This allows localhost clients to read or modify sensitive files (for example .env, *.pem, *.key) through the restored HTTP API even though those paths are treated as protected elsewhere, creating a secret-exposure and credential-tampering gap.

Useful? React with 👍 / 👎.

@justrach justrach merged commit 814c03a into main Apr 30, 2026
1 check passed
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.

MCP stdio: failure-path status text is written to stdout and corrupts JSON-RPC

1 participant