Skip to content

Phase 2.5: DNS-based interception, SQLite storage, new providers#4

Merged
GeiserX merged 6 commits intomainfrom
phase-2.5/dns-interception-storage
Apr 15, 2026
Merged

Phase 2.5: DNS-based interception, SQLite storage, new providers#4
GeiserX merged 6 commits intomainfrom
phase-2.5/dns-interception-storage

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented Apr 15, 2026

Summary

  • DNS-based interception: Replace IP-based host routes with /etc/hosts sentinel IPs + DoH resolution + native TCP forwarder (IP_BOUND_IF). Solves AWS Bedrock and Cursor rotating IP issues — DNS interception catches all traffic regardless of IP rotation.
  • Native C forwarder (agenttap-fwd): connects via IP_BOUND_IF on physical interface (en0), bypassing lo0 entirely. Universal binary (arm64 + x86_64).
  • Helper upgraded: manages /etc/hosts entries (# BEGIN/END AgentTap block) + DNS cache flush via dscacheutil -flushcache + killall -HUP mDNSResponder.
  • DoH resolver: Cloudflare 1.1.1.1 + Google 8.8.8.8 fallback — bypasses /etc/hosts for real upstream IP resolution.
  • SQLite trace storage: WAL mode, session detection (5-min gap), source app detection from User-Agent (Claude Code, Cursor, Aider, Copilot, etc.).
  • New providers: AWS Bedrock (us-east-1, us-west-2, eu-west-1) and Cursor (api2.cursor.sh), both enabled by default.
  • Debug server endpoints: /traces, /traces/:id, /sessions, /stats for querying stored traces.
  • ROADMAP expanded: auto-discovery section restructured into passive/active/UX phases.

Phases covered

  • Phase 2 completion: DNS-based transparent capture, native forwarder, helper daemon, Bedrock+Cursor
  • Phase 3 partial: SQLite storage, session management, token extraction, query API

Architecture change

Before: DNS resolve → host routes → per-IP PF rdr → proxy → direct upstream
After:  /etc/hosts sentinel → single PF rdr on lo0 → proxy → DoH → native fwd (IP_BOUND_IF) → upstream

Test plan

  • Build native binaries: cd native && make build-helper && make build-fwd
  • Start app: bunx electrobun dev
  • Enable capture via tray or curl localhost:19876/start
  • Verify /etc/hosts entries added for enabled providers
  • Run curl -sk https://api.anthropic.com/v1/messages and confirm trace captured
  • Query traces: curl localhost:19876/traces
  • Check sessions: curl localhost:19876/sessions
  • Check stats: curl localhost:19876/stats
  • Stop capture and verify /etc/hosts entries cleaned up
  • Verify no lingering processes: lsof -i :8443 -i :18443

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AWS Bedrock and Cursor providers; UI reflects domain-based captures.
    • Introduced a native TCP forwarder for pinned-interface upstreams and DoH-based domain resolution.
    • Enabled persistent trace recording with searchable sessions and a debug HTTP interface.
  • Bug Fixes / Reliability

    • Switched interception to hosts+PF sentinel mappings and improved macOS network/interface handling.
    • Hardened CA trust and helper installation flows.
  • Documentation

    • Expanded macOS interception and development guidance.

…sor providers

Replace IP-based host routes with /etc/hosts sentinel IPs + DoH resolution +
native TCP forwarder using IP_BOUND_IF for lo0 bypass. This solves AWS Bedrock
and Cursor rotating IP issues — DNS interception catches all traffic regardless
of IP rotation.

- Helper now manages /etc/hosts entries (BEGIN/END AgentTap block) + DNS flush
- DoH resolver (Cloudflare + Google fallback) bypasses /etc/hosts for real IPs
- Native C forwarder (agenttap-fwd) with IP_BOUND_IF on physical interface
- SQLite trace storage with WAL mode, session detection, query endpoints
- Add Bedrock (us-east-1, us-west-2, eu-west-1) and Cursor providers
- Debug server: /traces, /traces/:id, /sessions, /stats endpoints
- Source app detection from User-Agent (Claude Code, Cursor, Aider, etc.)
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Warning

Rate limit exceeded

@GeiserX has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 44 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 44 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3356d3ae-359f-41a8-982b-d07acd1f12d7

📥 Commits

Reviewing files that changed from the base of the PR and between 943b6fe and e463021.

📒 Files selected for processing (13)
  • native/Helper/HelperTool.swift
  • src/bun/dns/dns-cache.ts
  • src/bun/index.ts
  • src/bun/net/interface-bind.ts
  • src/bun/net/interface-detector.ts
  • src/bun/proxy/proxy-server.ts
  • src/bun/proxy/upstream-forwarder.ts
  • src/bun/storage/trace-db.test.ts
  • src/bun/storage/trace-db.ts
  • src/bun/storage/trace-writer.ts
  • src/bun/system/pf-manager.ts
  • src/bun/system/privilege-executor.ts
  • src/bun/system/redirect-manager.ts
📝 Walkthrough

Walkthrough

Adds DNS-based macOS transparent interception with a native interface‑bound TCP forwarder, DoH resolution, host-sentinel /etc/hosts mapping + PF rdr anchor, CA trust/hosts management, Bun FFI interface binding, DoH resolver, trace persistence (SQLite), and provider/provider-discovery additions (Bedrock, Cursor).

Changes

Cohort / File(s) Summary
Docs & Roadmap
AGENTS.md, ROADMAP.md
macOS interception guide, deprecated IP-based interception, DoH/upstream bypass flow, Bun/macOS caveats, phased auto-discovery plan and UX items.
Version & Packaging
Resources/version.json, electrobun.config.ts, package.json, .github/workflows/*
Added static version file (0.3.0), bumped app/package versions, added CI forwarder build/test steps, and included native artifacts into app bundle.
Native Forwarder & Helper
native/Forwarder/agenttap-fwd.c, native/Helper/HelperTool.swift, native/Makefile
New agenttap-fwd C forwarder (IP_BOUND_IF), helper tool updated (v1.5.0) to manage PF anchors, CA and hosts blocks, scoped routes; Makefile builds universal forwarder.
Forwarder CI / Release
.github/workflows/build.yml, .github/workflows/release.yml
Build-forwarder job, artifact upload/download, embedding and verification of native-forwarder in app bundle.
Bun Native Bridge & Loader
src/bun/native-bridge.ts
Adjusted app-contents detection and dev-mode project-root search for locating bundled native dylibs.
DoH & DNS
src/bun/dns/doh-resolver.ts, src/bun/dns/doh-resolver.test.ts
Added DoH resolver with caching/stale-window, multi-provider fallback (Cloudflare/Google), and unit test.
Interface Utilities
src/bun/net/interface-bind.ts, src/bun/net/interface-detector.ts
FFI-based interface binding helpers (create/bind/close fd) and detector logic updated to exclude utun/tun, plus getInterfaceForIP/getGatewayForInterface helpers.
Upstream Forwarding
src/bun/proxy/upstream-forwarder.ts, src/bun/proxy/transparent-proxy.ts
New upstream-forwarder process manager (spawn, readiness probe, stop), transparent proxy now starts/stops forwarder by ifaceName instead of binding localAddress.
Proxy Server Changes
src/bun/proxy/proxy-server.ts
Removed localAddress upstream binding; upstream flow now: DoH → local forwarder handshake → TLS over forwarded socket. EventStream handling/ token extraction extended for amazonaws/cursor.
System / Helper Client
src/bun/system/helper-client.ts, src/bun/system/pf-manager.ts
Helper client version gating, new RPCs (verifyAnchors, CA/hosts management), PF manager switched to domain→sentinel /etc/hosts mapping + single rdr anchor, verify/cleanup adapted.
Redirect Manager
src/bun/system/redirect-manager.ts
Replaced IP-based DNS workflows with domain list tracking, added CA trust propagation (NODE_EXTRA_CA_CERTS), watchdog verify/repair, stronger cleanup checks.
Trace Storage & Writer
src/bun/storage/trace-db.ts, src/bun/storage/trace-writer.ts, src/bun/storage/trace-db.test.ts, src/bun/storage/trace-writer.ts
SQLite-backed trace DB with sessions, pruning, query APIs; trace-writer persists sanitized traces and session association; tests added for session behavior.
Runtime / Index
src/bun/index.ts
Initialize/close trace DB and writer on startup/shutdown, debug HTTP server token auth and endpoints, provider toggle blocked while capture active, sanitized trace URL logging.
SSE Reassembly & Tests
src/bun/sse/sse-reassembler.ts, src/bun/sse/sse-reassembler.test.ts
Line-ending normalization, route amazonaws→Anthropic, cursor→OpenAI reassembly paths; added CRLF parse test.
CA & Misc
src/bun/ca/ca-manager.ts, src/shared/types.ts
CA dir creation permissions tightened; added bedrock and cursor providers to DEFAULT_PROVIDERS.
Bun FFI & Tests
src/bun/net/*, src/bun/*
Multiple Bun FFI and test additions covering interface bind, DoH, SSE, storage. See cohorts above for specifics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main changes: DNS-based interception mechanism, new SQLite trace storage, and two new AI providers (Bedrock/Cursor).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-2.5/dns-interception-storage

Comment @coderabbitai help to get the list of available commands and usage tips.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Full review triggered.

…p to v0.3.0

- Bedrock and Cursor providers disabled by default — Node.js apps need restart
  after NODE_EXTRA_CA_CERTS is set via launchctl, so enabling these by default
  breaks already-running Claude Code/Cursor sessions
- Remove rejectUnauthorized:false on upstream TLS — the forwarder tunnel passes
  real server certs through correctly (verified with Anthropic + OpenAI)
- Align version to 0.3.0 across package.json, electrobun.config.ts, version.json
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bun/proxy/proxy-server.ts (1)

266-365: ⚠️ Potential issue | 🟠 Major

Don’t finalize normal HTTP responses on socket shutdown.

This handler still treats end/close as a successful completion path for non-SSE traffic, so an upstream disconnect or mid-stream TLS failure can be stored as a complete trace with a truncated body. Keep shutdown-based finalization limited to SSE and only mark normal responses complete once Content-Length or the chunked terminator has been satisfied.

As per coding guidelines, "Never rely on HTTP end/close events for response completion; detect completion by Content-Length, chunked encoding terminator 0\r\n\r\n, or connection close for SSE".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/proxy/proxy-server.ts` around lines 266 - 365, The handler finalizes
and records non-SSE responses on upstream 'end'/'close', allowing truncated
traces; change wireUpstreamHandlers so finalize() is only invoked on
'end'/'close' when isSSE is true. Remove finalize() calls from
upstream.on("end") and upstream.on("close") for non-SSE flows (just end/destroy
clientSocket), rely on the existing content-length and CHUNKED_TERMINATOR logic
in the upstream.on("data") path (which uses parseResponseHead, contentLength,
isChunked, dechunk and CHUNKED_TERMINATOR) to call finalize() when the response
is actually complete, and keep finalize() guarded by the finalized flag.
🧹 Nitpick comments (8)
src/bun/proxy/upstream-forwarder.ts (1)

31-44: Add production guard for CWD-derived path search.

native-bridge.ts explicitly checks isDev() before searching CWD-derived paths to "avoid library hijack in production". This file always walks up from process.cwd(), which could allow execution of a malicious binary if bundle paths are missing.

The current order (bundle paths first) mitigates this, but adding an explicit dev-mode check provides defense in depth.

Proposed fix
+function isDev(): boolean {
+  return !import.meta.dir.includes(".app/Contents/");
+}
+
 function findForwarderBinary(): string | null {
   const name = "agenttap-fwd";
   const candidates = [
     // App bundle: Contents/MacOS/
     join(import.meta.dir, "..", "..", "MacOS", name),
     // App bundle: Contents/Resources/
     join(import.meta.dir, "..", "Resources", name),
   ];

-  // Dev mode: native/build/
-  let dir = process.cwd();
-  for (let i = 0; i < 10; i++) {
-    try {
-      const stat = Bun.file(join(dir, "package.json"));
-      if (stat.size > 0) {
-        candidates.push(join(dir, "native", "build", name));
-        break;
-      }
-    } catch { /* keep walking */ }
-    const parent = join(dir, "..");
-    if (parent === dir) break;
-    dir = parent;
+  // Only search CWD-derived paths in dev mode (avoid binary hijack in production)
+  if (isDev()) {
+    let dir = process.cwd();
+    for (let i = 0; i < 10; i++) {
+      try {
+        const stat = Bun.file(join(dir, "package.json"));
+        if (stat.size > 0) {
+          candidates.push(join(dir, "native", "build", name));
+          break;
+        }
+      } catch { /* keep walking */ }
+      const parent = join(dir, "..");
+      if (parent === dir) break;
+      dir = parent;
+    }
   }

   return candidates.find((p) => existsSync(p)) ?? null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/proxy/upstream-forwarder.ts` around lines 31 - 44, The CWD upward
walk that pushes candidates based on process.cwd() should only run in
development; add an explicit isDev() guard around the loop that walks from
process.cwd() so production avoids deriving paths from the CWD (leaving existing
candidate order and bundle-path checks intact). Specifically, wrap the existing
loop that references process.cwd(), join(dir, "package.json"), candidates, and
name with a check like if (isDev()) { ... } so the code path that searches
parent directories is skipped in production.
src/bun/net/interface-bind.ts (2)

90-92: Consider logging if SO_REUSEADDR fails.

The return value is discarded. While non-critical, logging at debug level helps diagnose obscure socket issues.

-  lib.symbols.setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ptr(oneBuf), 4);
+  const reuseResult = lib.symbols.setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ptr(oneBuf), 4) as number;
+  if (reuseResult !== 0) {
+    console.debug(`[InterfaceBind] SO_REUSEADDR failed (non-fatal)`);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/net/interface-bind.ts` around lines 90 - 92, Check the return value
from the lib.symbols.setsockopt call that sets SO_REUSEADDR (the line using fd,
SOL_SOCKET, SO_REUSEADDR, ptr(oneBuf), 4) and log a debug message if it
indicates failure; specifically capture the integer return and, on non-zero,
call the module's debug logger (or console.debug) with a clear message including
fd and the errno/return code to aid diagnosis. Ensure you still pass ptr(oneBuf)
and keep the current behavior if setsockopt succeeds.

109-117: Bind failure is silently swallowed.

Comment says it's non-fatal, but the caller has no way to know whether the socket is bound to the requested port or an ephemeral one. Consider returning a status object or logging at debug level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/net/interface-bind.ts` around lines 109 - 117, The bind failure is
currently swallowed in interface-bind.ts (makeSockaddrIn + lib.symbols.bind
returning bindResult) so callers cannot tell whether the socket was bound to the
requested localPort or an ephemeral port; update the function that currently
returns fd to return a status object (e.g., { fd, bound: boolean, requestedPort:
number, actualPort?: number }) or at minimum return a tuple/[fd, bound] and set
bound = bindResult === 0, and add a debug log when bindResult !== 0; ensure
callers of this function are updated to handle the new return shape and/or check
the bound flag.
src/bun/system/pf-manager.ts (2)

42-46: Unused parameters: iface and gateway.

These are declared but never used in the function body. Leftover from IP-based approach?

🧹 Remove if not needed
 export async function applyRules(
   domains: string[],
-  iface?: string,
-  gateway?: string,
 ): Promise<boolean> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/system/pf-manager.ts` around lines 42 - 46, The applyRules function
declares unused parameters iface and gateway; to fix, either remove these
parameters from the function signature and update any callers to stop passing
them, or if they were intended to affect behavior, wire them into the function
logic (e.g., use iface to select the interface and gateway to set
route/next-hop) and ensure tests/call sites pass the appropriate values;
reference the applyRules(domains: string[], iface?: string, gateway?: string)
declaration when making the change.

117-126: Condition commands.length > 0 is always true.

The array is initialized with one element on line 118, so this check never fails.

-    const commands = [`pfctl -a "${ANCHOR_NAME}" -F all 2>/dev/null`];
-    if (commands.length > 0) {
-      const result = await executePrivileged(commands.join(" ; ") + " ; true");
+    const result = await executePrivileged(`pfctl -a "${ANCHOR_NAME}" -F all 2>/dev/null ; true`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/system/pf-manager.ts` around lines 117 - 126, The condition checking
commands.length > 0 is redundant because commands is initialized with one
element; remove the array-length guard and simply invoke executePrivileged with
the constructed command for clearing the PF anchor, or build the commands array
only when needed; update the pf-clear block around ANCHOR_NAME to call
executePrivileged(commands.join(" ; ") + " ; true") directly and keep the
existing error handling that logs result.error on failure (ensure you update the
code paths in pf-manager.ts where commands and executePrivileged are used).
src/bun/storage/trace-writer.ts (1)

34-54: Traces queryable by provider, model, timestamp — sourceApp via session JOIN.

Per the coding guideline, traces must be queryable by provider, model, timestamp, and source app. The trace record stores provider, model, timestamp, and session_id. Source app is on the session, requiring a JOIN for that query dimension.

This is a reasonable design (avoids duplicating sourceApp on every trace), but confirm the query patterns in trace-db.ts support this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/storage/trace-writer.ts` around lines 34 - 54, The trace record
currently omits source_app (stored on sessions), so update the trace read/query
functions (e.g., queryTraces / getTraces in trace-db.ts) to JOIN the sessions
table on trace.session_id and include sessions.source_app in the SELECT and
WHERE clauses so queries can filter by provider, model, timestamp and
source_app; ensure the JOIN uses trace.session_id = sessions.id and that any
pagination/sort logic still uses trace.timestamp; keep insertTrace unchanged but
add necessary tests for filtering by source_app.
src/bun/index.ts (1)

296-301: Consider validating trace ID format.

The ID is extracted directly from the URL path and passed to getTraceById. If IDs have a known format (e.g., UUID), validating prevents unnecessary DB queries for malformed input.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/index.ts` around lines 296 - 301, The code extracts `id` via `const
id = path.slice("/traces/".length)` and calls `getTraceById(id)` without
validating format; add a validation step (e.g., UUID regex or your service's ID
pattern) before calling `getTraceById`, return a 400 Bad Request (Response.json
with status 400) for malformed IDs, and only call `getTraceById` when the ID
passes validation; update the block that checks `if
(path.startsWith("/traces/")) { ... }` accordingly.
src/bun/net/interface-detector.ts (1)

26-32: Minor: en0 may be checked twice in the fallback loop.

The array ["en0", ...Object.keys(ifaces).filter((n) => n.startsWith("en")).sort()] includes en0 explicitly, then again from the filter if it exists. Not a bug (first match returns), but slightly redundant.

🔧 Optional fix
-  for (const enName of ["en0", ...Object.keys(ifaces).filter((n) => n.startsWith("en")).sort()]) {
+  const enInterfaces = Object.keys(ifaces).filter((n) => n.startsWith("en")).sort();
+  // Prioritize en0 by moving it to front if present
+  const orderedEn = enInterfaces.includes("en0")
+    ? ["en0", ...enInterfaces.filter((n) => n !== "en0")]
+    : enInterfaces;
+  for (const enName of orderedEn) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/net/interface-detector.ts` around lines 26 - 32, The loop that
prefers "en0" uses ["en0", ...Object.keys(ifaces).filter((n) =>
n.startsWith("en")).sort()] which can include "en0" twice; update the iterable
used in the for (const enName of ...) to dedupe "en0" (e.g., filter out "en0"
from the spread or construct a Set) so "en0" appears only once when iterating
over ifaces; adjust the code around the for loop that references ifaces and
enName to use the deduped list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@native/Helper/HelperTool.swift`:
- Around line 314-325: Replace the loose ip.hasPrefix("127.0.1.") check in the
entries validation loop with a strict IPv4 literal check: match the whole string
against a pattern like ^127\.0\.1\.(\d{1,3})$ (or use an IPv4 parser) then parse
the captured last octet and ensure it is within the allowed sentinel range (e.g.
1..254); keep returning the same error shape on failure. Update the validation
in the for entry in entries loop (variables ip, domain, id) in HelperTool.swift
so malformed values like 127.0.1.999 are rejected and only valid 127.0.1.x
addresses with the bounded last octet pass.
- Around line 127-148: verifyAnchors currently reads arbitrary anchorName from
params without checking the allowlist, letting callers query any PF anchor; add
the same allowlist guard used in applyPfRules() and clearRules() by calling
HelperTool.isAllowedAnchor(anchorName) after extracting anchorName and return an
error response (e.g., ["id": id, "success": false, "error": "Not allowed
anchor"]) when it fails; ensure the early-return shape matches the existing
responses so rdrResult/filterResult are only executed for allowed anchors.
- Around line 109-110: Do not unconditionally flip net.inet.ip.forwarding;
instead read the current value with run("/usr/sbin/sysctl", args: ["-n",
"net.inet.ip.forwarding"]) before changing in the code that currently uses
run("/usr/sbin/sysctl", args: ["-w", "net.inet.ip.forwarding=1"]) (and the
counterpart that writes it back), only call the sysctl write if the current
value is 0, and store that original value in a persistent/local state so the
clear/teardown path restores it to the original value (rather than always
forcing 0); apply the same read-then-conditional-write-and-restore logic for the
related write at the other occurrence (lines ~219-220).

In `@src/bun/proxy/proxy-server.ts`:
- Around line 245-250: The upstream TLS connection currently disables
certificate verification by setting rejectUnauthorized: false in the tls.connect
call that creates upstream; change this to enforce verification
(rejectUnauthorized: true) and remove the blanket TODO; if you need a temporary
bypass, gate it behind an explicit, opt-in debug/config flag (e.g., an env var
like ALLOW_INSECURE_UPSTREAM or a runtime option) checked where tls.connect is
called (use UPSTREAM_TIMEOUT, domain and the existing tunnel variable), so
production defaults always verify certificates.

In `@src/bun/proxy/upstream-forwarder.ts`:
- Around line 80-86: The exit handler for fwdProcess can race with
stopForwarder() and log an "unexpected exit" even when stopForwarder
intentionally cleared it; to fix, capture the current fwdProcess reference when
you register the exited callback (e.g., const proc = fwdProcess) and inside the
fwdProcess.exited.then handler only log/clear if the global fwdProcess still
equals that captured reference (and then set it to null), so the handler ignores
exits for processes already cleared by stopForwarder; update the handler around
fwdProcess.exited and ensure stopForwarder continues to set fwdProcess = null.

In `@src/bun/storage/trace-db.ts`:
- Around line 80-106: The activeSessions Map is only keyed by provider, so
getOrCreateSession merges sessions across different source_app values; change
the cache key to include sourceApp (e.g., composite key `${provider}:${sourceApp
?? ''}`) and update all usages in getOrCreateSession (lookup, update, closing,
and set) to use that composite key so sessions are tracked per
provider+sourceApp while keeping the same SESSION_GAP_MS logic and DB
INSERT/UPDATE behavior; ensure ActiveSession entries still contain provider and
sourceApp fields and that the timestamp/closing logic uses the matched entry.
- Around line 194-207: The current API only exposes
getRecentTraces/getTraceById/getRecentSessions and lacks filtering by provider,
model, timestamp range, and source app; implement a new query function (e.g.,
getTracesFiltered or extend getRecentTraces) that accepts optional filters:
provider, model, startTimestamp, endTimestamp, sourceApp, limit, offset; build a
parameterized SQL SELECT that applies WHERE clauses only for provided filters
and, to support source_app, JOIN traces to sessions on session_id
(sessions.source_app) so traces can be filtered by source app; ensure the
function returns the same Record<string, unknown>[] shape and uses
getDB().query(...).all(...) with proper parameter binding and ordering by
timestamp DESC.

In `@src/bun/system/redirect-manager.ts`:
- Around line 144-156: verifyAndRepairRules currently only verifies PF rules via
verifyRulesActive() and therefore misses DNS/hosts interception breakage; add a
DNS interception health check (e.g., verifyDnsInterception or extend
verifyRulesActive to include DNS) and call it alongside verifyRulesActive()
inside verifyAndRepairRules, and when DNS check fails invoke the corresponding
repair routine (e.g., repairDnsInterception or extend applyRules to restore
hosts/AgentTap entries using activeDomains) so both PF and DNS states are
validated and repaired. Include references to verifyAndRepairRules,
verifyRulesActive, applyRules and activeDomains when making the change.
- Around line 100-109: When applyRules(allDomains) fails after
setNodeCATrust(certPath) has already run, revert the NODE_EXTRA_CA_CERTS change
so new Node/Bun processes don't continue trusting the AgentTap CA; update the
flow around setNodeCATrust, applyRules, and stopTransparentProxy so that on
pf/apply failure you call a rollback function (e.g., unsetNodeCATrust or restore
previous NODE_EXTRA_CA_CERTS value) and ensure cleanup runs before returning the
error. Locate setNodeCATrust(certPath) and add a failure-path call to the
rollback/unset routine (and/or wrap in try/finally) so the launchctl environment
is restored when applyRules or subsequent steps fail.
- Around line 176-191: The current setNodeCATrust and clearNodeCATrust always
overwrite/unset the launchd NODE_EXTRA_CA_CERTS which loses any preexisting user
setting; change them to preserve and restore the prior value: in setNodeCATrust
call launchctl getenv NODE_EXTRA_CA_CERTS (e.g. via Bun.spawnSync) and store the
returned value in a module-level variable (e.g. previousNodeExtraCACerts) before
calling launchctl setenv to the new certPath; in clearNodeCATrust, if
previousNodeExtraCACerts is non-empty call launchctl setenv to restore it,
otherwise call launchctl unsetenv, and update logs accordingly; keep using the
existing function names setNodeCATrust and clearNodeCATrust.

---

Outside diff comments:
In `@src/bun/proxy/proxy-server.ts`:
- Around line 266-365: The handler finalizes and records non-SSE responses on
upstream 'end'/'close', allowing truncated traces; change wireUpstreamHandlers
so finalize() is only invoked on 'end'/'close' when isSSE is true. Remove
finalize() calls from upstream.on("end") and upstream.on("close") for non-SSE
flows (just end/destroy clientSocket), rely on the existing content-length and
CHUNKED_TERMINATOR logic in the upstream.on("data") path (which uses
parseResponseHead, contentLength, isChunked, dechunk and CHUNKED_TERMINATOR) to
call finalize() when the response is actually complete, and keep finalize()
guarded by the finalized flag.

---

Nitpick comments:
In `@src/bun/index.ts`:
- Around line 296-301: The code extracts `id` via `const id =
path.slice("/traces/".length)` and calls `getTraceById(id)` without validating
format; add a validation step (e.g., UUID regex or your service's ID pattern)
before calling `getTraceById`, return a 400 Bad Request (Response.json with
status 400) for malformed IDs, and only call `getTraceById` when the ID passes
validation; update the block that checks `if (path.startsWith("/traces/")) { ...
}` accordingly.

In `@src/bun/net/interface-bind.ts`:
- Around line 90-92: Check the return value from the lib.symbols.setsockopt call
that sets SO_REUSEADDR (the line using fd, SOL_SOCKET, SO_REUSEADDR,
ptr(oneBuf), 4) and log a debug message if it indicates failure; specifically
capture the integer return and, on non-zero, call the module's debug logger (or
console.debug) with a clear message including fd and the errno/return code to
aid diagnosis. Ensure you still pass ptr(oneBuf) and keep the current behavior
if setsockopt succeeds.
- Around line 109-117: The bind failure is currently swallowed in
interface-bind.ts (makeSockaddrIn + lib.symbols.bind returning bindResult) so
callers cannot tell whether the socket was bound to the requested localPort or
an ephemeral port; update the function that currently returns fd to return a
status object (e.g., { fd, bound: boolean, requestedPort: number, actualPort?:
number }) or at minimum return a tuple/[fd, bound] and set bound = bindResult
=== 0, and add a debug log when bindResult !== 0; ensure callers of this
function are updated to handle the new return shape and/or check the bound flag.

In `@src/bun/net/interface-detector.ts`:
- Around line 26-32: The loop that prefers "en0" uses ["en0",
...Object.keys(ifaces).filter((n) => n.startsWith("en")).sort()] which can
include "en0" twice; update the iterable used in the for (const enName of ...)
to dedupe "en0" (e.g., filter out "en0" from the spread or construct a Set) so
"en0" appears only once when iterating over ifaces; adjust the code around the
for loop that references ifaces and enName to use the deduped list.

In `@src/bun/proxy/upstream-forwarder.ts`:
- Around line 31-44: The CWD upward walk that pushes candidates based on
process.cwd() should only run in development; add an explicit isDev() guard
around the loop that walks from process.cwd() so production avoids deriving
paths from the CWD (leaving existing candidate order and bundle-path checks
intact). Specifically, wrap the existing loop that references process.cwd(),
join(dir, "package.json"), candidates, and name with a check like if (isDev()) {
... } so the code path that searches parent directories is skipped in
production.

In `@src/bun/storage/trace-writer.ts`:
- Around line 34-54: The trace record currently omits source_app (stored on
sessions), so update the trace read/query functions (e.g., queryTraces /
getTraces in trace-db.ts) to JOIN the sessions table on trace.session_id and
include sessions.source_app in the SELECT and WHERE clauses so queries can
filter by provider, model, timestamp and source_app; ensure the JOIN uses
trace.session_id = sessions.id and that any pagination/sort logic still uses
trace.timestamp; keep insertTrace unchanged but add necessary tests for
filtering by source_app.

In `@src/bun/system/pf-manager.ts`:
- Around line 42-46: The applyRules function declares unused parameters iface
and gateway; to fix, either remove these parameters from the function signature
and update any callers to stop passing them, or if they were intended to affect
behavior, wire them into the function logic (e.g., use iface to select the
interface and gateway to set route/next-hop) and ensure tests/call sites pass
the appropriate values; reference the applyRules(domains: string[], iface?:
string, gateway?: string) declaration when making the change.
- Around line 117-126: The condition checking commands.length > 0 is redundant
because commands is initialized with one element; remove the array-length guard
and simply invoke executePrivileged with the constructed command for clearing
the PF anchor, or build the commands array only when needed; update the pf-clear
block around ANCHOR_NAME to call executePrivileged(commands.join(" ; ") + " ;
true") directly and keep the existing error handling that logs result.error on
failure (ensure you update the code paths in pf-manager.ts where commands and
executePrivileged are used).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 648e61f6-d805-419a-8aa4-49eb1b54f419

📥 Commits

Reviewing files that changed from the base of the PR and between 1d5d465 and 3d42e30.

📒 Files selected for processing (20)
  • AGENTS.md
  • ROADMAP.md
  • Resources/version.json
  • native/Forwarder/agenttap-fwd.c
  • native/Helper/HelperTool.swift
  • native/Makefile
  • src/bun/dns/doh-resolver.ts
  • src/bun/index.ts
  • src/bun/net/interface-bind.ts
  • src/bun/net/interface-detector.ts
  • src/bun/proxy/proxy-server.ts
  • src/bun/proxy/transparent-proxy.ts
  • src/bun/proxy/upstream-forwarder.ts
  • src/bun/sse/sse-reassembler.ts
  • src/bun/storage/trace-db.ts
  • src/bun/storage/trace-writer.ts
  • src/bun/system/helper-client.ts
  • src/bun/system/pf-manager.ts
  • src/bun/system/redirect-manager.ts
  • src/shared/types.ts

Comment thread native/Helper/HelperTool.swift Outdated
Comment thread native/Helper/HelperTool.swift
Comment thread native/Helper/HelperTool.swift
Comment thread src/bun/proxy/proxy-server.ts Outdated
Comment thread src/bun/proxy/upstream-forwarder.ts
Comment thread src/bun/storage/trace-db.ts Outdated
Comment thread src/bun/storage/trace-db.ts Outdated
Comment thread src/bun/system/redirect-manager.ts
Comment thread src/bun/system/redirect-manager.ts
Comment thread src/bun/system/redirect-manager.ts
@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Repo Mapping Lane]

Inspected hot spots

  • proxy-server.ts: DoH → forwarder tunnel → tls.connect upstream; hard dependency on isForwarderRunning() and FWD_PORT.
  • transparent-proxy.ts / upstream-forwarder.ts: forwarder lifecycle + binary discovery (native/build/agenttap-fwd, bundle paths).
  • redirect-manager.ts / pf-manager.ts: sentinel /etc/hosts + single lo0 PF rdr; rolls back hosts on PF failure.
  • helper-client.ts ↔ Swift helper: REQUIRED_VERSION must match helper; hosts + applyPfRules.
  • trace-writer.tstrace-db.ts via trace-emitter: sync path on every trace; WAL/session logic.

Suspicious coupling / regressions

  • src/bun/net/interface-bind.ts: no importers — parallel FFI approach vs agenttap-fwd; dead code risk / confusion.
  • dns-cache.ts + dns-resolver.ts: not wired to new DoH path (cache may be stale roadmap-only).
  • native-bridge.ts / CA / SNI unchanged but still on critical path; version drift if helper install fails silently.

Untouched but still load-bearing

  • ca-manager.ts / cert-cache.ts, sni-parser.ts, http-parser.ts, views/mainview — capture UX and TLS leaf issuance still depend on these.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Performance Lane]

I walked the request path end-to-end (transparent proxy -> DoH -> forwarder -> SSE buffering -> trace writer -> debug API). A few scale blockers stand out:

  1. High - Every request pays live DoH latency.
    src/bun/proxy/proxy-server.ts:193 calls resolveViaDoH() inline for each request, and src/bun/dns/doh-resolver.ts:32 sequentially hits remote DoH endpoints with 5s timeouts. There is no in-memory/TTL cache in this path, even though src/bun/dns/dns-cache.ts already exists. One slow DoH provider can add seconds of tail latency to otherwise-local proxying.

Fix: resolve on cache miss only, keep a TTL-based memory cache, and refresh stale entries in the background.

  1. High - Streaming responses are buffered multiple times in RAM and then written twice to SQLite/WAL.
    wireUpstreamHandlers() accumulates every chunk in responseChunks and only finalizes SSE on connection close (src/bun/proxy/proxy-server.ts:271-350). On finalize it builds bodyStr, parses SSE into chunks, and trace-writer.ts:47-48 persists both response_body and response_chunks. For large or long-lived streams this is raw buffers + UTF-8 string + parsed chunk array + JSON text, all scaling linearly with stream size.

Fix: add bounded/truncated capture, incremental SSE parsing/token accounting, and persist either reassembled body or chunk metadata for streams, not both.

  1. High - Trace persistence runs synchronously on the socket callback path.
    traceEmitter.emit("trace", trace) is synchronous (src/bun/capture/trace-emitter.ts:4-16), and startTraceWriter() immediately JSON-stringifies the payload and hits SQLite (src/bun/storage/trace-writer.ts:24-58). insertTrace() then does a trace insert plus session aggregate update (src/bun/storage/trace-db.ts:135-184), and getOrCreateSession() can add extra writes. Under concurrency, these sync DB writes will head-of-line block the Bun event loop and slow unrelated sockets.

Fix: move writes behind a bounded async queue/batcher and wrap the per-trace DB mutations in a single transaction.

  1. High - The native forwarder is fork-per-connection.
    agenttap-fwd does accept() + fork() for every tunnel (native/Forwarder/agenttap-fwd.c:237-248), and the child sticks around for the full relay lifetime. With concurrent SSE requests this becomes one OS process per live upstream. That is an early scalability ceiling on context switches, RSS, and process count.

Fix: switch to a single-process poll/kqueue loop or a fixed worker pool instead of fork() per connection.

  1. Medium - The debug/query APIs will get expensive fast and they share the same process as capture.
    src/bun/index.ts:290-309 exposes synchronous SQLite queries directly, with user-controlled limit/offset and no cap. getStats() does full-table aggregates on every call, and /traces/:id returns the full stored bodies/chunks (src/bun/storage/trace-db.ts:194-227). Large queries or repeated stats polling can block the same event loop that is proxying traffic.

Fix: cap limit, use keyset pagination, split summary vs full-body endpoints, and precompute or cache stats.

Some good foundations here: WAL + synchronous = NORMAL is the right SQLite baseline, the prepared insert statement is good, and the single-flight per-domain TLS setup avoids duplicate cert work. But I’d hold this PR until the hot-path DoH lookup, synchronous writes, and stream buffering are addressed.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Repo Mapping Lane]

Inspected hot spots

  • src/bun/proxy/proxy-server.ts + upstream-forwarder.ts: DoH → 127.0.0.1:9443 forwarder protocol (IP\n443\n) + tls.connect over tunnel — core interception path.
  • src/bun/system/pf-manager.ts + redirect-manager.ts: sentinel 127.0.1.0/24 + single PF rdr; helper /etc/hosts rollback on PF failure.
  • native/Helper/HelperTool.swift + helper-client.ts: version gate, addHostsEntries / removeHostsEntries.
  • native/Forwarder/agenttap-fwd.c: IP_BOUND_IF, handshake OK/ERR line.
  • src/bun/storage/trace-db.ts + trace-writer.ts + traceEmitter in index.ts: dual listeners on trace (UI + SQLite).

Suspicious coupling / regression risks

  • Dual traceEmitter subscribers (trace-writer + index.ts): ordering and duplicate work if extended carelessly.
  • dns-cache.ts / dns-resolver.ts: not imported by current Bun entry path — parallel DNS story vs DoH-only upstream; stale ROADMAP vs runtime.
  • interface-bind.ts: FFI IP_BOUND_IF appears superseded by C forwarder — dead path / confusion risk if someone wires both.
  • Forwarder binary discovery: multiple candidate paths; missing agenttap-fwd → silent upstream failure (isForwarderRunning guard in proxy).
  • Helper/PF partial failure: hosts applied before PF; rollback on applyPfRules failure — verify edge cases (helper down mid-session).

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Architecture Lane]

I reviewed the full capture/control path, with extra focus on the PR’s new DNS-based interception, helper lifecycle, forwarder, proxy data path, and SQLite storage. I specifically inspected src/bun/system/redirect-manager.ts, src/bun/system/pf-manager.ts, src/bun/system/helper-client.ts, native/Helper/HelperTool.swift, src/bun/proxy/transparent-proxy.ts, src/bun/proxy/proxy-server.ts, src/bun/proxy/upstream-forwarder.ts, src/bun/dns/doh-resolver.ts, src/bun/storage/trace-db.ts, and src/bun/storage/trace-writer.ts.

Main findings:

  1. The new DNS interception state is not treated transactionally. The design now depends on both PF and /etc/hosts, but startup cleanup and watchdog verification only reason about PF state. cleanupOnStartup() reaches cleanupStaleRules() before the helper is marked ready, and verifyRulesActive() only checks the PF anchor. That creates a rollback hazard where stale sentinel mappings can survive crashes/reboots or partial failures and keep provider domains blackholed even when capture is "off". I would treat this as the main blocker.

  2. The Bedrock/Cursor trust path mutates global launchd state lossy. setNodeCATrust() overwrites NODE_EXTRA_CA_CERTS globally and clearNodeCATrust() unconditionally unsets it, so AgentTap can destroy a pre-existing trust-chain configuration unrelated to this app. That is hidden coupling to the user’s wider Node/Bun environment.

  3. The new session model collapses independent clients into provider-wide sessions. getOrCreateSession() keys active sessions only by provider, while trace-writer infers source_app per trace. Two apps using the same provider within the gap window will be merged into one session and inherit whichever source_app created it first.

  4. The new upstream path adds a per-request uncached DoH dependency. proxy-server now calls resolveViaDoH() for every intercepted request, and doh-resolver has no cache / control-plane prefetch in this path. That adds latency to every request and makes capture availability depend on public resolvers even when system DNS is healthy.

  5. Storage/UI work now runs synchronously on the capture hot path. traceEmitter.emit("trace") fans out immediately to trace-writer (JSON stringify + SQLite insert + session aggregate updates) and the menu-bar state update listener. With large SSE bodies or bursty traffic, this will become head-of-line blocking.

Recommendations:

  • Treat PF rules, /etc/hosts, forwarder health, and any environment mutations as one capture-state transaction with symmetric startup/shutdown repair.
  • Preserve/restore any prior NODE_EXTRA_CA_CERTS value, or scope trust injection more narrowly.
  • Re-key sessions by at least provider + source app, and close active sessions on capture stop.
  • Move DNS resolution back into a cached/background control plane instead of the request hot path.
  • Queue or batch trace persistence and UI updates so the proxy path is not doing synchronous storage work.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[DNS/Redirect Trace Lane]

I traced the branch end-to-end from provider selection -> /etc/hosts sentinel mapping -> DNS cache flush -> PF redirect -> SNI extraction -> DoH lookup -> native forwarder -> upstream TLS. I found four places where the causal chain can break or stale state can survive.

  1. Stop/cleanup currently masks failures and can strand users in a broken state.
    disableTransparentCapture() awaits clearRules() but ignores its boolean result, then always stops the proxy and clears in-memory state. Inside clearRules(), the /etc/hosts removal result is also ignored. If either helper write fails, the app can report “stopped” while domains still resolve to 127.0.1.x or PF still redirects to 127.0.0.1:8443, with no proxy left to serve them.
    Files: src/bun/system/redirect-manager.ts, src/bun/system/pf-manager.ts, native/Helper/HelperTool.swift.

  2. Startup cleanup does not actually recover stale DNS/PF state after a crash/restart.
    main() calls cleanupOnStartup() before anything marks the helper ready. cleanupStaleRules() only removes the AgentTap /etc/hosts block and PF anchors when helperClient.isReady is already true, so on startup it will skip the exact cleanup this DNS-based design now depends on. In the old IP-route design that was mostly harmless; with sentinel /etc/hosts entries it means provider domains can keep resolving to loopback while AgentTap is inactive.
    Files: src/bun/index.ts, src/bun/system/redirect-manager.ts, src/bun/system/pf-manager.ts.

  3. The watchdog only verifies that the PF anchor still contains an rdr, not that the rest of the path is alive.
    verifyAndRepairRules() only calls verifyRulesActive(), which only checks pfctl -sn. It does not verify the AgentTap /etc/hosts block, the transparent listener on :8443, or the forwarder on :9443. If agenttap-fwd dies or the hosts block disappears, capture can remain “active” in UI while requests fail later in the chain.
    Files: src/bun/system/redirect-manager.ts, src/bun/system/pf-manager.ts, src/bun/proxy/upstream-forwarder.ts, src/bun/proxy/proxy-server.ts.

  4. The helper now toggles global net.inet.ip.forwarding even though the DNS-sentinel design no longer uses kernel forwarding to reach upstream.
    The new PF anchor is only rdr pass on lo0 ... 127.0.1.0/24 -> 127.0.0.1:8443, and the upstream escape hatch is the native forwarder’s IP_BOUND_IF. There is no route-to in the new data path, but applyPfRules() still forces ip.forwarding=1 and clearRules() forces it back to 0. That can clobber any pre-existing forwarding setup outside AgentTap.
    Files: src/bun/system/pf-manager.ts, native/Helper/HelperTool.swift, native/Forwarder/agenttap-fwd.c.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Privacy/Data Lane]

  1. HIGH — Unauthenticated localhost debug API can silently enable capture and expose full decrypted traces. src/bun/index.ts starts the debug server on app launch and exposes /start, /stop, /traces, /traces/:id, /sessions, and /stats with no auth. src/bun/storage/trace-db.ts#getTraceById() returns the full row, and src/bun/storage/trace-writer.ts persists raw request/response headers and bodies, so any local process that can reach localhost:19876 can turn capture on and then read prompts, completions, and provider credentials from trace detail responses.

  2. HIGH — The PR adds powerful root-helper commands to a socket that effectively trusts any normal local user process. native/Helper/HelperTool.swift#verifyClient() accepts any peer whose effective gid is 20 (staff), and this PR adds applyPfRules, addHostsEntries, removeHostsEntries, verifyAnchors, installCACert, and removeCACert to that channel. Combined with the existing root:staff helper socket in native/Helper/main.swift, another local process can drive privileged /etc/hosts and PF changes (and attempt CA trust operations) without going through the UI.

  3. MEDIUM — Trace storage is overly broad and has no retention/redaction boundary. src/bun/storage/trace-writer.ts stores request_headers, request_body, response_headers, response_body, and response_chunks verbatim, while src/bun/storage/trace-db.ts enables SQLite WAL mode and provides no purge/checkpoint/redaction path. That means bearer tokens / API keys plus full prompt and response content accumulate indefinitely in plaintext across traces.db and traces.db-wal, increasing accidental disclosure risk via local reads, backups, or crash artifacts.

  4. LOW — Source-app attribution can mix traces from different apps into one session. src/bun/storage/trace-db.ts#getOrCreateSession() keys active sessions only by provider, while src/bun/storage/trace-writer.ts#detectSourceApp() derives source_app from the first matching User-Agent. If two tools hit the same provider within the 5-minute gap, one session can contain both apps’ traces while keeping only the first app label, which makes later review/export easy to misinterpret.

Secrets/dependency pass: I did not find newly hardcoded credentials in the PR diff, and bun audit reports no known dependency vulnerabilities.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Storage/API Lane]

Reviewed the storage/query path in src/bun/storage/trace-db.ts, src/bun/storage/trace-writer.ts, the debug endpoints in src/bun/index.ts, src/bun/proxy/proxy-server.ts, and the related shared trace types/callers.

What I checked:

  • SQLite schema and write path for trace/session integrity
  • Session detection against concurrent and long-running real-world traces
  • /traces, /traces/:id, /sessions, and /stats query behavior
  • Pagination/input handling, unbounded reads, and storage growth risks
  • Raw data exposure on the new local query surface
  • SSE/streamed trace compatibility with the stored schema

Findings from this lane are being posted separately to the reviewer summary.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Proxy Path Lane]

Focused review on proxy-server.ts, transparent-proxy.ts, upstream-forwarder.ts, sse-reassembler.ts, and adjacent proxy helpers.

Findings:

  1. [HIGH] src/bun/proxy/sni-parser.ts:25 can throw on a partial ClientHello because it calls readUInt16BE(offset) without first proving that two bytes are available. With the DNS-based architecture, every intercepted TLS flow now depends on this parser; a fragmented handshake can take down the transparent proxy instead of simply waiting for more bytes. I reproduced this with a 45-byte prefix buffer, which raises RangeError: The value of \"offset\" is out of range. Fix: add an offset + 2 > data.length guard before reading the cipher-suite length (and ideally wrap extractSNI() at the call site defensively as well).

  2. [HIGH] src/bun/proxy/proxy-server.ts:281-284 and src/bun/proxy/proxy-server.ts:352-353 ignore downstream backpressure and then unconditionally destroy() the client socket on upstream close. On a slow consumer, clientSocket.write() can already be buffering; destroying the socket immediately after a clean upstream EOF can drop the queued tail of the response, including the end of a large JSON response or final SSE bytes. Fix: make the relay backpressure-aware (write() return value + drain, or pipe() if Bun allows it here), and reserve destroy() for error paths instead of the normal end/close path.

  3. [MEDIUM] src/bun/proxy/upstream-forwarder.ts:49-89 reports success immediately after Bun.spawn(). startTransparentProxy() and enableTransparentCapture() treat the forwarder as ready before it has actually bound 127.0.0.1:9443, so the first intercepted connections can race into ECONNREFUSED, and bind/exec failures are only noticed asynchronously after capture is already marked active. Fix: wait for an explicit ready signal from the child (or a successful loopback port probe) before returning success.

  4. [MEDIUM] src/bun/sse/sse-reassembler.ts:16 only splits frames on \n\n. If any upstream emits canonical CRLF-delimited SSE (\r\n\r\n), the parser collapses the whole stream into one block, which breaks event boundaries, [DONE] detection, and token extraction. I confirmed the current splitter leaves a CRLF-formatted stream as a single block. Fix: normalize \r\n to \n first, or split on /\r?\n\r?\n/.

Positives:

  • The DoH -> forwarder -> TLS layering is cleanly separated, which makes the new upstream path easy to reason about.
  • domainInFlight plus the cleanup generation guard is a solid way to avoid duplicate per-domain TLS server creation during concurrent handshakes.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Privilege Security Lane]

Reviewed the privileged helper, helper socket trust boundary, /etc/hosts mutation, PF anchor loading, CA trust flow, launchctl CA env injection, new localhost listeners, forwarder/proxy subprocesses, trace storage/debug endpoints, and crash/cleanup paths.

Blocking findings:

  1. Critical — helper socket auth is too broad for the new command set. native/Helper/main.swift exposes the root helper as root:staff 0660, and native/Helper/HelperTool.swift accepts any client whose gid is staff. This PR extends that same socket with installCACert, addHostsEntries, and applyPfRules, so any local staff user/process can install an arbitrary system-trusted root CA, rewrite /etc/hosts, or load attacker-controlled PF rules from a user-writable file.

  2. High — the new trace/debug API exposes captured secrets to any local client. src/bun/index.ts now adds unauthenticated /traces, /traces/:id, /sessions, and /stats endpoints on localhost, while src/bun/storage/trace-writer.ts and src/bun/storage/trace-db.ts persist full request/response headers and bodies. Any other local user/process can read prompts, model output, bearer tokens, cookies, and other captured data. /start and /stop are also still state-changing GETs, so a webpage can CSRF-trigger capture toggles if the helper is already installed.

  3. Medium — cleanup/global-state handling can leave the machine in a more dangerous state after failure. native/Helper/HelperTool.swift sets net.inet.ip.forwarding=1 before PF load and does not roll it back if rule installation fails, and src/bun/system/redirect-manager.ts sets NODE_EXTRA_CA_CERTS via launchctl but startup cleanup only removes PF/hosts state. A crash or partial failure can therefore leave packet forwarding enabled and future Node/Bun processes trusting the MITM CA longer than intended.

  4. Medium — the new forwarder is an unauthenticated local egress proxy. native/Forwarder/agenttap-fwd.c accepts arbitrary host and port lines from any localhost client, and src/bun/proxy/upstream-forwarder.ts/src/bun/proxy/proxy-server.ts do not constrain that to the configured provider list. While AgentTap is running, local processes can use it as a generic interface-bound TCP relay instead of only for approved AI upstreams.

Checks completed:

  • bun audit: no dependency vulnerabilities found.
  • Secret-pattern scan across the repo/PR diff: no hardcoded credentials found in the changes.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Verification Lane]

I reviewed this as a verification/completion pass, with focus on the PR’s claims around DNS-based interception, cleanup semantics, provider defaults, and session/cross-process state.

Verdict: the DNS-based design is present in code, but the PR’s completion claims are not fully substantiated yet. I would not treat this as merge-ready in its current state.

Key findings:

  1. Blocking: the shipped app artifact does not include the new agenttap-fwd runtime dependency.

    • startForwarder() hard-fails if the binary is missing.
    • package.json runs cd native && make, whose default target only builds the bridge dylib, not build-fwd.
    • The CI/release workflows build/embed the helper, but never build or copy agenttap-fwd into the app bundle.
    • Fresh local evidence: after bun run build, AgentTap-dev.app/Contents/MacOS and Contents/Resources contained no agenttap-fwd.
    • Result: the core DNS-interception path cannot actually relay upstream in packaged builds even though CI is green.
  2. Blocking: cleanup is not transactionally verified, so stale interception state can survive failures/crashes.

    • cleanupOnStartup() runs before the helper is marked ready, and cleanupStaleRules() explicitly skips cleanup in that state.
    • doDisable() ignores the boolean result from clearRules() and still reports capture disabled.
    • setNodeCATrust() runs before PF/hosts setup, but the failure path after applyRules() does not restore the previous environment.
    • Result: /etc/hosts, PF state, or NODE_EXTRA_CA_CERTS can be left behind while the app believes capture is off.
  3. Major: session state is provider-wide and process-local, not source-app-safe or cross-process durable.

    • activeSessions is an in-memory Map<string, ActiveSession> keyed only by provider.
    • Two apps using the same provider within the 5-minute gap window will be merged into one session and inherit whichever source_app created it first.
    • Restarting the app also loses that session continuity entirely.
    • Result: the new session/source-app reporting is not reliable for concurrent tools.
  4. Major: normal HTTP traces can still be finalized from socket shutdown, contrary to AGENTS.md.

    • AGENTS.md says non-SSE responses must only complete via Content-Length / chunked terminator, and only SSE should finalize on close.
    • proxy-server.ts still calls finalize() on end/close unconditionally.
    • Result: truncated non-SSE responses can be persisted as if they were complete traces.
  5. Minor: provider defaults in code match AGENTS.md, but not the current PR body.

    • Code has Bedrock/Cursor disabled by default.
    • AGENTS.md also says they are disabled by default because Node/Bun clients need a restart after NODE_EXTRA_CA_CERTS is set.
    • The PR body still says they are enabled by default.
    • Result: the implementation matches repo guidance, but the PR description does not.

Fresh verification evidence:

  • bun test → no tests found.
  • bunx tsc --noEmit → fails, including errors in modified files such as src/bun/index.ts, src/bun/proxy/proxy-server.ts, and src/bun/proxy/transparent-proxy.ts.
  • bun run build → exits 0, but this does not prove the feature is shippable because the forwarder is not bundled.

Remaining uncertainty:

  • The runtime architecture change itself looks directionally correct (/etc/hosts sentinel IPs + DoH + forwarder), but without the packaging fix and stronger cleanup/session guarantees I can’t say the behavioral claims are fully verified.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Native Code Lane]

Findings after reviewing native/Helper/HelperTool.swift, native/Forwarder/agenttap-fwd.c, native/Helper/main.swift, native/Makefile, the launchd helper path, and the Bun/native call sites:

  1. HIGH - agenttap-fwd is never built or bundled in the official app path. src/bun/proxy/upstream-forwarder.ts requires a packaged agenttap-fwd, but package.json, .github/workflows/build.yml, and .github/workflows/release.yml only build/copy the bridge + helper. Release builds will enable capture, then fail upstream because the binary is missing.
  2. HIGH - Crash cleanup is incomplete. cleanupOnStartup() calls cleanupStaleRules(), but cleanupStaleRules() skips cleanup unless helperClient.isReady is already true. On a post-crash restart that flag is still false even if the helper is installed, so stale /etc/hosts sentinel mappings / PF state can survive and blackhole provider domains until manual cleanup.
  3. HIGH - Helper authentication is too weak for the newly added privileged operations. native/Helper/main.swift exposes the socket as root:staff 0660, and verifyClient() accepts any staff peer. With the new addHostsEntries / installCACert / removeCACert commands, any local staff-owned process can drive the root helper to rewrite /etc/hosts and attempt trust-store changes.
  4. MEDIUM - The health model only verifies PF rdr state. If agenttap-fwd exits after startup, upstream-forwarder.ts just logs and nulls fwdProcess, while verifyRulesActive() / the watchdog still report healthy because PF rules remain loaded. The UI can stay “active” while requests fail until a manual restart.
  5. MEDIUM - HelperTool.applyPfRules() / clearRules() now toggle net.inet.ip.forwarding, but the new DNS/sentinel path only installs an rdr rule. That unnecessary global sysctl change can leak across crashes and can wrongly force forwarding back to 0 on stop for users who already had it enabled.

Checked items:

  • Forwarder protocol and relay loop
  • Helper launchd install/reinstall path
  • Socket auth / permission model
  • Crash/restart cleanup
  • Forwarder watchdog / bundle assumptions
  • No linter issues on touched files

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Repo Mapping Lane]

Hot spots reviewed: redirect-manager.ts (orchestrates PF + proxy + iface + helper), pf-manager.ts + HelperTool.swift (/etc/hosts, PF, forwarding sysctl), proxy-server.ts (DoH → connectViaForwarder → TLS → traceEmitter), upstream-forwarder.ts + agenttap-fwd.c (IP_BOUND_IF tunnel protocol), trace-db.ts / trace-writer.ts + index.ts debug API, interface-detector.ts / interface-bind.ts.

Untouched but regression-relevant: dns-resolver.ts + dns-cache.ts (parallel DNS path; cache appears unused in src/ — dead code / stale ROADMAP risk), ca-manager.ts / cert-cache.ts (leaf MITM still feeds all upstream paths), http-parser.ts / sse-reassembler.ts (trace completion + token extraction), native-bridge.ts (FFI; unrelated to capture path), views/mainview (no trace viewer UI yet).

Suspicious coupling: Single traceEmitter fan-out to tray logger + SQLite writer (ordering/backpressure); DoH failure aborts upstream before forwarder; detectProvider/extractModel tied to DEFAULT_PROVIDERS domains; helper version REQUIRED_VERSION must stay in sync with Swift; forwarder binary discovery + FWD_PORT must match agenttap-fwd and PF/proxy ports.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Test Engineering Lane]

Highest-risk coverage gaps:

  1. Restart safety is not covered, and the current startup cleanup path looks unable to remove stale PF /etc/hosts state after a crash because cleanupOnStartup() runs before ensureHelperInstalled(), so cleanupStaleRules() logs and skips helper-backed cleanup when helperClient.isReady is false (src/bun/index.ts, src/bun/system/pf-manager.ts). Please add an integration test for "restart after crash leaves no stale hosts/PF state" and consider connecting to the helper before cleanup.

  2. The new forwarder path has no automated coverage in CI/release packaging, and the protocol/readiness path is still manual-only. package.json still runs cd native && make (which only builds universal), while .github/workflows/build.yml/release.yml only build/embed the helper, not agenttap-fwd, even though src/bun/proxy/upstream-forwarder.ts requires that binary at runtime. I’d add a build assertion that the forwarder is built and present in the app bundle, plus a small protocol test for OK/ERR handshake and child bind failure.

  3. Session correctness is under-tested and likely wrong across source apps. src/bun/storage/trace-writer.ts detects sourceApp, but getOrCreateSession() in src/bun/storage/trace-db.ts keys active sessions only by provider, so two apps hitting Anthropic within 5 minutes will be merged and the persisted source_app will be whichever app created the session first. Please add unit coverage for same-provider/different-source-app and session-gap boundaries.

  4. Helper version mismatch and DNS flush behavior are still manual-only. There is no test that a mismatched helper version triggers reinstall, or that /etc/hosts add/remove both flush DNS and roll back cleanly on failure (src/bun/system/helper-client.ts, native/Helper/HelperTool.swift). A focused mocked-helper suite would catch these regressions cheaply.

  5. Provider toggles and debug endpoints need state-transition coverage. /start has transition guards, /stop does not, and provider toggles mutate state.providers without any automated proof of the expected behavior when capture is already active (src/bun/index.ts, src/shared/types.ts, src/bun/system/redirect-manager.ts). I’d add a small integration harness around /start, /stop, /status, /traces, /sessions, and /stats, plus a toggle test that verifies whether changes apply immediately or only after restart.

Coverage that looks acceptable:

  • The pure metadata/version bumps (package.json, electrobun.config.ts, Resources/version.json) do not need dedicated tests beyond a consistency/build check.

I did not find any existing automated test suite in the repo, and CI currently runs build-only jobs.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Critic Lane]

Verdict: REJECT

This PR improves the happy path, but it also hardens several fragile assumptions into the architecture and introduces at least two release-blocking failures.

Critical blockers

  1. Packaged builds will not be able to capture traffic. The new runtime path now requires agenttap-fwd (src/bun/proxy/transparent-proxy.ts -> startForwarder() -> src/bun/proxy/upstream-forwarder.ts), but the normal build/release path still does not build or bundle that binary. package.json still uses build:native = cd native && make, and native/Makefile keeps all: universal, which only builds the Swift bridge. The release/build workflows bundle the helper, but not agenttap-fwd. That means a shipped app can install the helper and still fail at startTransparentProxy() because the forwarder binary is missing.

  2. The PR turns the debug server into an unauthenticated trace exfiltration API exposed beyond loopback. main() always calls startDebugServer() in src/bun/index.ts. That server now exposes /traces, /traces/:id, /sessions, and /stats, while trace-writer.ts persists raw headers and bodies. Bun.serve({ port }) is called without hostname; Bun's documented default is 0.0.0.0, even though the log says localhost. So this is not just local debugging hygiene — it is LAN-visible access to captured prompts, responses, and auth headers, plus unauthenticated /start and /stop state changes.

Major issues

  1. NODE_EXTRA_CA_CERTS is clobbered globally with no preservation or rollback safety. src/bun/system/redirect-manager.ts calls launchctl setenv NODE_EXTRA_CA_CERTS <cert> on enable and unconditional unsetenv on disable. That assumes AgentTap owns the variable. If the user already had NODE_EXTRA_CA_CERTS for corporate/internal PKI, AgentTap will overwrite it and later delete it. That can break unrelated Node/Bun processes and is hard to debug because the failure shows up in other tools.

  2. Session detection is architecturally wrong for a multi-agent tracer. src/bun/storage/trace-db.ts keys active sessions only by provider (Map<string, ActiveSession>), while trace-writer.ts detects source app but does not include it in the session key. Two concurrent Anthropic or OpenAI clients within 5 minutes get merged into one session, and the first source_app wins. This poisons the new storage layer with incorrect grouping, incorrect attribution, and misleading stats.

  3. The root helper is exposed to any local staff process, not just AgentTap. In native/Helper/main.swift, the socket is created root:staff with mode 0660, and verifyClient() in HelperTool.swift accepts any client in GID 20 (staff) or root. That means any local process running under a normal macOS user in staff can drive a root daemon that edits /etc/hosts, loads pf rules, and flips sysctls. That is too broad for a privileged helper.

  4. The helper still flips global IP forwarding on/off even though the new DNS+forwarder design no longer depends on it. native/Helper/HelperTool.swift sets net.inet.ip.forwarding=1 in applyPfRules() and forces it back to 0 in clearRules(). The current architecture in AGENTS.md uses /etc/hosts + PF rdr + native forwarder with IP_BOUND_IF; it is not using the old route-to bypass. So this is stale global mutation with real blast radius: if the user already had IP forwarding enabled for another reason, AgentTap will silently disable it on stop.

Residual risks / missing safeguards

  • No schema versioning or migrations for the new SQLite store.
  • No retention/redaction guardrails for persisted raw traces.
  • No automated test/CI assertion that a packaged app can actually enable capture end-to-end.
  • Product copy still overpromises “zero per-app configuration” while AGENTS.md documents the Bedrock/Cursor restart caveat.

What I would require before merging

  • Build, package, and assert the presence of agenttap-fwd in dev/build/release artifacts.
  • Remove or lock down the debug HTTP API before release: bind explicitly to 127.0.0.1 at minimum, and preferably gate it behind dev mode or auth.
  • Stop globally clobbering NODE_EXTRA_CA_CERTS; preserve/restore prior state or scope the trust workaround more narrowly.
  • Redesign session keys so concurrent apps do not get merged by provider.
  • Tighten helper authorization and stop mutating net.inet.ip.forwarding unless you can prove the current architecture still needs it.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Architecture Lane]

I did a full architecture pass over the redirect, helper, proxy/forwarder, DoH, CA, and storage layers in this branch.

I found four architecture issues worth fixing before this lands:

  1. Blocker — the new global interception state is not crash-safe or self-healing.
    The PR changed capture enablement from a mostly PF/route concern into a 3-part state machine: /etc/hosts sentinel mappings, PF anchor state, and launchctl NODE_EXTRA_CA_CERTS. But startup/watchdog verification still models only PF. applyRules() mutates both /etc/hosts and PF (src/bun/system/pf-manager.ts:59-100), while verifyRulesActive() checks only PF (src/bun/system/pf-manager.ts:133-137) and cleanupStaleRules() only attempts real cleanup when helperClient.isReady is already true (src/bun/system/pf-manager.ts:147-170). On startup we call cleanupOnStartup() before anything makes the helper ready (src/bun/index.ts:345-347, src/bun/system/helper-client.ts:137-158). That means a crash/partial failure can leave stale sentinel mappings or the launchctl CA env behind, and the watchdog can report “healthy” while non-PF state is wrong.

  2. High — the helper still toggles machine-wide net.inet.ip.forwarding, even though this DNS-based design no longer needs kernel forwarding.
    The new PF anchor is just an rdr to 127.0.0.1:8443 (src/bun/system/pf-manager.ts:71-80), and upstream bypass is handled in user space by the forwarder. But the helper still forces net.inet.ip.forwarding=1 on apply and =0 on clear (native/Helper/HelperTool.swift:109-118, native/Helper/HelperTool.swift:216-220). That is now an unrelated global side effect, and disabling capture can break a machine that already had forwarding enabled for some other purpose.

  3. High — every intercepted request now depends on live uncached DoH to Cloudflare/Google.
    Before opening the upstream TLS connection, relayToUpstream() always calls resolveViaDoH(domain) and fails closed if those lookups fail (src/bun/proxy/proxy-server.ts:191-204). resolveViaDoH() has no cache, no TTL handling, no stale fallback, and hardcodes direct queries to 1.1.1.1 / 8.8.8.8 (src/bun/dns/doh-resolver.ts:8-56). The previous DNS path had cached + stale-fallback behavior (src/bun/dns/dns-cache.ts:45-65), but the proxy hot path no longer uses it. So each captured API request now pays an extra external DNS RTT/failure dependency and leaks provider domains to third-party resolvers regardless of the user’s system DNS choice.

  4. Medium — storage/UI work now runs synchronously on the proxy hot path.
    traceEmitter is a normal synchronous EventEmitter (src/bun/capture/trace-emitter.ts:4-16), and proxy-server emits directly from finalize() in the response-completion path (src/bun/proxy/proxy-server.ts:316-349). The listeners then serialize full bodies/chunks, do synchronous SQLite writes (src/bun/storage/trace-writer.ts:24-58, src/bun/storage/trace-db.ts:135-184), and rebuild the tray menu (src/bun/index.ts:349-363). That tightly couples storage/UI latency to request completion, which will show up as avoidable backpressure once traces get larger or concurrent.

Residual risk I inspected but do not consider a blocker for this PR: the proxy core now imports the macOS-only forwarder/DoH strategy directly (src/bun/proxy/transparent-proxy.ts, src/bun/proxy/proxy-server.ts), so Linux support will likely require surgery inside the proxy path rather than a narrow platform adapter.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Proxy Path Lane]

I traced the transparent path end to end: transparent-proxy.ts SNI extraction -> proxy-server.ts domain TLS -> DoH lookup -> upstream-forwarder.ts / agenttap-fwd.c tunnel -> upstream TLS -> response finalization -> sse-reassembler.ts.

  1. [HIGH] Bedrock streaming is not protocol-correct yet. wireUpstreamHandlers() only treats text/event-stream as streaming (src/bun/proxy/proxy-server.ts:291), then finalizes all other bodies as JSON/chunked text (src/bun/proxy/proxy-server.ts:316-347). The new Bedrock alias in reassembleSSE() (src/bun/sse/sse-reassembler.ts:51-55) never helps for the real Bedrock streaming path, because Bedrock streams over application/vnd.amazon.eventstream rather than SSE. That means Bedrock traces will be finalized as binary eventstream bytes / UTF-8 garbage instead of parsed message chunks + usage, which is a blocker for the new provider support in this PR. Fix: add explicit Bedrock eventstream detection and frame parsing keyed off Content-Type: application/vnd.amazon.eventstream and x-amzn-bedrock-content-type, instead of routing amazonaws through the Anthropic SSE path.

  2. [MEDIUM] Forwarder startup is racy. startForwarder() reports success immediately after Bun.spawn() (src/bun/proxy/upstream-forwarder.ts:49-89), while the relay path only checks isForwarderRunning() before dialing 127.0.0.1:9443 (src/bun/proxy/proxy-server.ts:185-223). If the child has not finished binding yet, or exits immediately because startup failed, the first relayed request can still hit ECONNREFUSED even though startup succeeded. Fix: wait for an explicit ready signal (stderr line / port probe) and fail startup if the child exits before readiness.

  3. [MEDIUM] The new native forwarder still has an unbounded write/backpressure path. In relay(), the child blocks in a plain write() loop (native/Forwarder/agenttap-fwd.c:118-123) after polling only for readable fds (native/Forwarder/agenttap-fwd.c:108-129). A slow or stalled peer can hang a child indefinitely in write(), so RELAY_TIMEOUT_MS does not actually cap the full relay path. Fix: keep the sockets non-blocking, poll for POLLOUT, and enforce a write deadline while draining partial writes.

Positive note: the SNI extraction path itself looked sound in this pass. Buffering the ClientHello in transparent-proxy.ts before calling extractSNI(), then fanning out into per-domain TLS terminators, is internally consistent and I did not spot a domain-extraction bug in the touched code.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[DNS/Redirect Trace Lane]

Traced path: provider toggle -> /etc/hosts sentinel mapping -> DNS cache flush -> PF rdr -> TLS SNI -> DoH -> agenttap-fwd -> upstream TLS.

Blocking findings:

  1. Startup cleanup is effectively a no-op for stale DNS/PF state. main() calls cleanupOnStartup() before ensureHelperInstalled(), and cleanupStaleRules() now relies on an unprivileged pfctl -a ... -sn probe to detect stale PF. On macOS that probe returns Permission denied, so a crash can leave the AgentTap /etc/hosts block and/or PF anchor behind on restart. Because the PR removed the old persisted route-state path, there is no fallback signal anymore. Affected: src/bun/index.ts, src/bun/system/pf-manager.ts, native/Helper/HelperTool.swift.

  2. Stop/disable can silently leave stale sentinel mappings behind. clearRules() calls helperClient.removeHostsEntries() but ignores its success field, then doDisable() ignores the overall clearRules() result and marks capture stopped anyway. In the new DNS design, that means /etc/hosts can keep resolving provider domains to 127.0.1.x after PF is cleared, blackholing traffic while the UI says capture is off. Affected: src/bun/system/pf-manager.ts, src/bun/system/redirect-manager.ts.

  3. The helper now toggles global net.inet.ip.forwarding even though the new anchor only installs rdr pass on lo0 ... -> 127.0.0.1:8443. There is no route-to or forwarding-based PF path left in the PR, so clearRules() can disable a forwarding configuration the machine already had before AgentTap ran. Affected: native/Helper/HelperTool.swift, src/bun/system/pf-manager.ts.

Risky transition edge:

  1. agenttap-fwd start-up isn't synchronized and stale forwarders aren't cleaned up on startup. startForwarder() returns success immediately after Bun.spawn(), without waiting for the child to bind 127.0.0.1:9443; isForwarderRunning() only checks that local process handle. If a previous agenttap-fwd survived a crash or 9443 is occupied, the app can proceed to install DNS/PF state and only fail later when the first upstream connect happens. The repo's own AGENTS.md already notes that agenttap-fwd may survive crashes, so this path needs explicit recovery. Affected: src/bun/proxy/upstream-forwarder.ts, src/bun/proxy/transparent-proxy.ts, src/bun/proxy/proxy-server.ts.

What I did verify as coherent:

  • /etc/hosts writes and removals do flush dscacheutil + mDNSResponder.
  • SNI extraction feeds the real domain into DoH and upstream TLS (servername: domain), so the happy-path domain chain is conceptually correct.
  • Both DoH endpoints used by the PR returned 200 with Accept: application/dns-json.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Native Code Lane]

I concentrated on the native/helper lane (native/Helper/*, native/Forwarder/*, native/Makefile) plus the Bun/native integration and launchd assumptions.

  • [HIGH] agenttap-fwd is never bundled into packaged app artifacts, so the new path only works in dev. src/bun/proxy/upstream-forwarder.ts looks for the binary inside the app bundle, but .github/workflows/build.yml and .github/workflows/release.yml still only build/embed com.geiserx.agenttap.helper. A release build will hit [Forwarder] Binary not found and fail to start capture. Fix: add make build-fwd to CI/release, publish it as an artifact, copy it into the app bundle, and codesign it with the app.
  • [HIGH] Crash/restart cleanup is not complete. src/bun/index.ts calls cleanupOnStartup() before the helper is installed/ready, and src/bun/system/pf-manager.ts skips cleanup when helperClient.isReady is false. On top of that, the legacy cleanup path calls helperClient.clearRules(OLD_ANCHOR_NAME, []), but native/Helper/HelperTool.swift only deletes host routes that are explicitly passed in ips. After a crash/upgrade, stale /etc/hosts sentinel entries and/or legacy per-IP host routes can survive and blackhole provider traffic until the user manually repairs the machine. Fix: make startup cleanup able to reach the helper before returning, and migrate/delete legacy routes explicitly instead of passing an empty list.
  • [HIGH] Forwarder readiness/lifecycle is not verified. src/bun/proxy/upstream-forwarder.ts returns success immediately after Bun.spawn(), src/bun/proxy/transparent-proxy.ts proceeds without waiting for a ready signal, and stopForwarder() only kills the tracked child. If an old agenttap-fwd survives a crash and still owns port 9443, the replacement child exits after bind() but capture can still move forward with a broken in-memory state. Likewise, if proxy startup fails after the forwarder was spawned, the forwarder is leaked. Fix: add a readiness handshake/ping, fail startup unless the forwarder is actually accepting connections, and detect/reuse/kill stale listeners on startup.
  • [MEDIUM] The helper still flips global net.inet.ip.forwarding on/off in native/Helper/HelperTool.swift, even though the new DNS plus local forwarder design no longer uses route-to or host-route forwarding. That clobbers any pre-existing system forwarding configuration and can leave forwarding enabled after a crash or disable it for unrelated tooling on stop. Fix: remove the sysctl mutations, or at minimum snapshot and restore the previous value instead of forcing 1 and 0.

Checked items:

  • Forwarder protocol and Bun-side handshake assumptions
  • LaunchDaemon install/bootstrap path
  • Crash/restart cleanup path
  • Architecture-specific packaging assumptions
  • Lint/diagnostic pass on the touched native/helper integration files

Positive notes:

  • REQUIRED_VERSION on the helper protocol is a good guard against silent client/helper drift.
  • Restricting DoH resolution to A records matches the forwarder AF_INET implementation, so there is no hidden IPv6 mismatch in the current path.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Privilege Security Lane]

I found security blockers in the privilege boundary and local-control surface.

  1. The root helper is still reachable by any local staff user/process because the socket is exposed as root:staff 0660 and verifyClient() only checks GID 20. This PR expands that surface by adding new root-capable commands in native/Helper/HelperTool.swift (addHostsEntries, removeHostsEntries, installCACert, removeCACert, verifyAnchors) while native/Helper/main.swift still accepts any staff client.

  2. The helper command surface is not scoped to app-owned inputs. applyPfRules() accepts any absolute existing anchorPath, addHostRoutes() still exposes route mutation, and installCACert() accepts an arbitrary certificate path. In combination with the socket trust model above, a non-root local attacker can drive root-owned PF/routing/trust changes by talking to /var/run/com.geiserx.agenttap.helper.sock directly.

  3. Cleanup/rollback is incomplete. cleanupOnStartup() can detect stale PF state but skips cleanup unless helperClient.isReady, doDisable() ignores clearRules() failure, and the helper unconditionally flips net.inet.ip.forwarding on/off instead of restoring prior state. A crash or partial failure can leave PF, /etc/hosts, forwarding, or related capture state behind.

  4. The debug HTTP surface remains unauthenticated local IPC. I verified Bun binds it to localhost, but the new /traces, /traces/:id, /sessions, and /stats endpoints expose full captured request/response data to any local process, while /start and /stop still let localhost callers trigger privileged capture state changes. Because these are GET endpoints with no auth, browser-driven localhost CSRF is also possible for the side-effectful routes.

What I checked:

  • Privileged helper socket permissions, peer verification, and install/update path
  • PF anchor loading, /etc/hosts mutation, route manipulation, and IP forwarding changes
  • CA trust installation/removal paths and NODE_EXTRA_CA_CERTS handling
  • Subprocess/shell execution (pfctl, route, security, launchctl, osascript)
  • Local IPC surfaces: helper socket, debug HTTP server, native forwarder listener
  • Cleanup behavior on startup/shutdown/failure paths
  • Dependency audit (bun audit: no vulnerabilities)
  • Targeted secrets scan in the reviewed codepath (no concrete hardcoded secrets found)

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Storage/API Lane]

Checked:

  • src/bun/storage/trace-db.ts schema, session model, query helpers, and write/update flow
  • src/bun/storage/trace-writer.ts call path and source-app/session assignment
  • debug/query endpoints in src/bun/index.ts
  • trace shapes and upstream callers in src/bun/capture/types.ts, src/bun/proxy/proxy-server.ts, and src/bun/sse/sse-reassembler.ts
  • IDE diagnostics on the modified storage/API files (ReadLints: no linter errors)

Findings:

  1. HIGH – session boundaries can be corrupted by out-of-order completion.

    • src/bun/storage/trace-writer.ts:28-32 passes trace.request.timestamp into getOrCreateSession().
    • src/bun/storage/trace-db.ts:82-95 then uses that start timestamp both to decide whether a session is still active and to overwrite existing.lastTraceAt.
    • That means a long request that started earlier but finishes later can rewind lastTraceAt, so the next request can spuriously roll over to a new session, and ended_at can move backwards to a time earlier than traces already recorded in the same session.
    • Fix: base session activity on completion time (request.timestamp + durationMs or emit time), and never let lastTraceAt decrease.
  2. HIGH – the debug API exposes raw secrets and trace contents over an unauthenticated HTTP surface.

    • src/bun/storage/trace-writer.ts:43-48 persists raw request/response headers and bodies.
    • src/bun/storage/trace-db.ts:200-201 returns SELECT * for /traces/:id.
    • src/bun/index.ts:240-316 exposes /traces, /traces/:id, /sessions, and /stats with no auth, no redaction, and no explicit hostname on Bun.serve(...).
    • In practice this leaks Authorization headers, cookies, prompts, model output, etc. to any caller that can reach the debug port.
    • Fix: bind explicitly to 127.0.0.1, require an auth token for debug endpoints, and redact sensitive headers before storing/serving.
  3. MEDIUM – session identity is keyed only by provider, so unrelated apps get merged and source_app becomes misleading.

    • src/bun/storage/trace-db.ts:80-106 uses Map<string, ActiveSession> keyed only by provider.
    • If Cursor and Claude Code both hit Anthropic within 5 minutes, they will share one session row and the first app to create the session wins the source_app field.
    • Fix: key active sessions by (provider, sourceApp) or a richer discriminator that matches the intended session semantics.
  4. MEDIUM/traces pagination is not stable enough for real trace volume, and the query limits are unbounded.

    • src/bun/storage/trace-db.ts:194-197 orders by timestamp DESC only, then paginates with LIMIT/OFFSET.
    • Millisecond timestamps are not unique under concurrent capture, so page boundaries can skip/duplicate rows when multiple traces share the same timestamp.
    • src/bun/index.ts:290-305 also accepts arbitrary limit/offset, so one request can force a very large synchronous SQLite read + JSON serialization.
    • Fix: add a stable secondary sort (timestamp DESC, id DESC) or switch to cursor pagination, and clamp limit to a small maximum.
  5. MEDIUM – streaming traces are stored twice with no retention/size guard, so the DB will grow quickly on real workloads.

    • src/bun/storage/trace-db.ts:29-33 stores both response_body and response_chunks.
    • src/bun/storage/trace-writer.ts:47-48 writes the full raw SSE body and a JSON array of every parsed SSE chunk.
    • For long streams this duplicates payload content and adds per-chunk timestamp overhead, which will bloat both the DB file and /traces/:id responses.
    • Fix: store one representation (or truncate/compress one of them), and add retention/max-body policies before this sees sustained usage.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Performance Lane]

Reviewed with a performance/scale lens, focusing on request-path overhead, per-request DoH, forwarder lifecycle/readiness, SQLite/WAL behavior, trace buffering, SSE handling, and debug/query APIs.

Blocking findings:

  1. HIGH - Per-request DoH lookup sits directly on the hot path with no cache or stale fallback.

    • src/bun/proxy/proxy-server.ts:191-204
    • src/bun/dns/doh-resolver.ts:32-56
    • Every intercepted request waits on an external DoH fetch before the upstream TCP/TLS handshake can even begin, and the current fallback chain can still cost up to two 5s timeouts. That makes resolver latency part of every request and turns a transient DoH outage into total capture failure.
    • Suggestion: cache answers by domain+TTL, prewarm/refresh asynchronously when capture is enabled, and keep a last-known-good fallback so one resolver hiccup does not fail the request path.
  2. HIGH - The proxy effectively disables HTTP keep-alive, so every request repays the full setup cost.

    • src/bun/proxy/proxy-server.ts:302-310
    • src/bun/proxy/proxy-server.ts:352-353
    • Once a non-SSE response is considered complete, the code finalizes and destroys the upstream socket, then closes the client side on end/close. That means no request/response reuse on the same TLS session: each subsequent request has to reconnect, re-resolve, re-enter the forwarder, and renegotiate TLS.
    • Suggestion: keep the client/upstream pair alive and implement a proper request/response loop per decrypted connection, only closing when the peer closes or protocol semantics require it.
  3. HIGH - Large bodies and streams are fully buffered, repeatedly copied, and then duplicated again for storage.

    • src/bun/proxy/proxy-server.ts:143-160
    • src/bun/proxy/proxy-server.ts:281-327
    • src/bun/proxy/proxy-server.ts:339-343
    • src/bun/sse/sse-reassembler.ts:14-42
    • src/bun/storage/trace-writer.ts:47-48
    • The request path uses Buffer.concat() repeatedly while accumulating bodies, SSE responses stay resident until close, and then the same payload is kept as raw response_body plus parsed response_chunks. With real prompt sizes or long Cursor/Claude streams, this becomes O(n^2) copying plus multiple full in-memory copies of the same trace.
    • Suggestion: switch to incremental parsing/counters, cap or truncate stored bodies, and store either normalized SSE chunks or a normalized body view rather than both raw and expanded forms.
  4. MEDIUM - Trace persistence is synchronous and write-amplified, even though WAL is enabled.

    • src/bun/proxy/proxy-server.ts:349
    • src/bun/storage/trace-writer.ts:24-58
    • src/bun/storage/trace-db.ts:82-107
    • src/bun/storage/trace-db.ts:135-185
    • src/bun/index.ts:355-364
    • traceEmitter.emit() is synchronous, so trace finalization blocks on JSON serialization, SQLite writes, session aggregate updates, logging, and a tray rebuild. Each trace also does multiple autocommit writes (INSERT traces, UPDATE sessions, plus session open/close churn), which will serialize throughput on the main process under sustained load.
    • Suggestion: enqueue traces to a background writer, batch or transaction-wrap the trace/session updates, and debounce tray/menu refreshes instead of doing them per trace.
  5. MEDIUM - Forwarder startup is not readiness-checked, and steady-state scale is one process per tunnel.

    • src/bun/proxy/upstream-forwarder.ts:49-90
    • native/Forwarder/agenttap-fwd.c:237-248
    • startForwarder() returns success immediately after Bun.spawn(), but capture can start before the child is actually listening on 9443. After that, each accepted tunnel forks a dedicated child process, so long-lived SSE sessions scale linearly as OS processes.
    • Suggestion: wait for an explicit ready signal before enabling capture, and consider replacing the fork-per-connection model with a single-process evented relay or at least a bounded worker pool.
  6. MEDIUM - The debug/query endpoints will struggle once trace bodies become large.

    • src/bun/index.ts:290-309
    • src/bun/storage/trace-db.ts:194-201
    • /traces accepts arbitrary limit values, /traces/:id returns the full row including large request/response bodies and chunk blobs, and Response.json() has to materialize the entire payload in memory before sending it.
    • Suggestion: clamp server-side limits, keep list/detail endpoints body-free by default, and require an explicit/capped body fetch path for large payloads.

Positive notes:

  • src/bun/storage/trace-db.ts enabling WAL + synchronous=NORMAL + busy_timeout is the right baseline for SQLite here.
  • insertTrace() uses a prepared statement, and getRecentTraces() avoids selecting the body columns on the summary path.

Recommendation: REQUEST CHANGES

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Test Engineering Lane]

I did not find adequate automated coverage for any of the requested areas. The closest guard is the existing macOS build workflow, but it still does not compile or bundle the new forwarder, so even packaging/build verification is not yet adequate.

  1. High — the new forwarder is not build-packaged or CI-verified. package.json still runs cd native && make, and native/Makefile defaults all to universal, which only builds libAgentTapBridge.dylib. Neither .github/workflows/build.yml nor .github/workflows/release.yml calls make build-fwd or embeds native/build/agenttap-fwd, even though src/bun/proxy/upstream-forwarder.ts hard-requires that binary at runtime.

  2. High — provider toggles are only cosmetic while capture is active. src/bun/index.ts flips provider.enabled in tray state, but never re-enters enableTransparentCapture()/applyRules(). src/bun/system/redirect-manager.ts snapshots activeProviders/activeDomains only during doEnable(), so the UI can show a provider as disabled while PF + /etc/hosts interception is still active until the user stops and restarts capture.

  3. High — session correctness is under-tested and currently merges different apps together. src/bun/storage/trace-db.ts keys active sessions only by provider, while src/bun/storage/trace-writer.ts separately infers sourceApp. Two different clients hitting the same provider within 5 minutes get folded into one session, and the first client wins source_app.

  4. High — SSE parsing is brittle and has no fixture coverage. src/bun/sse/sse-reassembler.ts splits raw streams on "\n\n" only. Real SSE bodies commonly arrive as CRLF-delimited blocks, so this can fail to split events correctly and break chunk/token extraction for Anthropic/OpenAI/Cursor streams. I would add fixtures for LF vs CRLF, multi-line data: fields, [DONE], and usage-only terminal chunks.

  5. Medium — the debug API is not safety-tested and is likely network-exposed. src/bun/index.ts always starts the debug server, exposes mutating GET endpoints (/start, /stop) plus raw trace/session reads, and never sets a hostname. Bun.serve({ port }) defaults to 0.0.0.0, so this is likely reachable beyond loopback unless explicitly locked down.

  6. Medium — startup cleanup / stale-state recovery / DNS fallback remain untested. src/bun/system/pf-manager.ts only removes stale /etc/hosts entries when helperClient.isReady; on startup that is false, so a prior crash can leave sentinel mappings behind until capture is re-enabled. src/bun/dns/doh-resolver.ts returns [] after two DoH failures, and src/bun/proxy/proxy-server.ts immediately drops the client connection instead of falling back to system DNS or cached answers. native/Helper/HelperTool.swift also ignores failures from dscacheutil / mDNSResponder, so DNS flush success is never verified.

  7. Medium — helper mismatch/reinstall and forwarder readiness paths have no regression harness. src/bun/system/helper-client.ts has no tests for version mismatch, canceled install prompt, stale socket, or delayed daemon startup. src/bun/proxy/upstream-forwarder.ts treats Bun.spawn() as readiness and never waits for the port bind or an application-level health signal, so the first captured request can race the forwarder startup.

If you want a minimal first-pass test plan, I’d start with: (a) a CI assertion that the app artifact contains agenttap-fwd, (b) an integration test proving provider toggles actually reapply interception while capture is live, and (c) fixture-based tests for session bucketing and SSE parsing.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented Apr 15, 2026

[Privacy/Data Lane]

I reviewed the PR with a privacy/data-handling focus: storage, logging, debug endpoints, helper trust boundaries, source-app detection, trace fields, retention assumptions, and accidental leakage paths.

Findings:

  1. High - unauthenticated debug API is exposed more broadly than intended (src/bun/index.ts, startDebugServer())

    • Bun.serve({ port: DEBUG_PORT, ... }) is started unconditionally from main() and does not set hostname: "127.0.0.1" or any auth.
    • In Bun, omitting hostname defaults to 0.0.0.0, so /traces, /traces/:id, /sessions, /stats, /start, and /stop become reachable from other machines if the host firewall allows it.
    • Because /traces/:id returns the full stored row, this is effectively a remote unauthenticated dump of decrypted prompts/responses/headers plus remote control of capture state.
    • Suggested fix: only enable this server in explicit dev builds, bind it to loopback, and require authentication for any non-local diagnostics.
  2. High - root helper authorization is too broad for a system interception component (native/Helper/main.swift, native/Helper/HelperTool.swift, verifyClient())

    • The helper socket is created at /var/run/com.geiserx.agenttap.helper.sock with chmod(0o660) and chown(..., 0, 20), and verifyClient() accepts any peer whose gid is 20 (staff) or uid is 0.
    • On macOS, normal interactive users are typically in staff, so any local user/process that can reach the socket can ask the root helper to rewrite /etc/hosts, load PF rules, and install/remove CA trust.
    • That is an over-broad local privilege boundary for a helper that controls system-wide interception.
    • Suggested fix: authenticate the client by code signature / audit token / blessed XPC identity instead of group membership, or otherwise scope access to the specific installing user only.
  3. Medium - full trace corpus is persisted in plaintext with no redaction or retention controls (src/bun/storage/trace-db.ts, src/bun/storage/trace-writer.ts)

    • The schema stores raw request_headers, request_body, response_headers, response_body, and response_chunks, and the writer serializes all of them verbatim.
    • That means the DB will keep Authorization headers, API keys, cookies, prompts, tool outputs, and model responses indefinitely on disk. There is no purge/TTL path, and /traces/:id re-exposes the full record.
    • For an app whose whole purpose is decrypted-trace capture, this may be useful operationally, but from a privacy lane it still needs explicit minimization and retention boundaries.
    • Suggested fix: redact auth/session headers before write, make full body capture opt-in or masked by default, add retention/expiry, and lock down file permissions for the DB and WAL sidecars.
  4. Low - trace logging still emits full request URLs (src/bun/index.ts)

    • [Trace] ... ${trace.request.url} ... sends the complete URL to stdout/stderr logs.
    • If a provider or upstream ever places opaque identifiers or signed params in the query string, those leak into logs unnecessarily.
    • Suggested fix: log origin + path only, or strip query strings before logging.

What I checked beyond the blockers:

  • Source-app detection only uses user-agent to derive a coarse label; I did not find extra host/process introspection beyond what is already present in captured headers.
  • The proxy and forwarder listeners themselves are loopback-bound, which is good; the main accidental network exposure I found is the debug HTTP server.
  • I did not find hardcoded credentials introduced by this PR.
  • bun audit reports no known dependency vulnerabilities on the current lockfile.

From a privacy/data lane, I would block on items 1 and 2 before shipping, and I would want item 3 resolved or explicitly scoped/documented before this feature is enabled for real user traffic.

Tighten helper access, cleanup, packaging, trace privacy, and forwarder reliability so capture failures do not leave stale state or leak local secrets. Add focused regression tests and CI coverage for session bucketing, SSE parsing, and DoH caching.
Fix the remaining actionable review items around truncated trace completion, DNS watchdog verification, NODE_EXTRA_CA_CERTS restoration, trace filtering, and helper validation. Tighten the debug/query surface and add regression coverage for source-app filtering while leaving low-value docstring churn out of this PR.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/bun/system/pf-manager.ts (1)

106-138: ⚠️ Potential issue | 🟠 Major

clearRules() can report success while /etc/hosts is still poisoned.

If helperClient.isReady is false, this path skips removeHostsEntries() entirely and can still return true after only flushing PF. Stopping capture then leaves the sentinel mappings behind and provider domains stay blackholed until a later cleanup. Treat helper unavailability as a failure here, or fall back to a privileged hosts cleanup path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/system/pf-manager.ts` around lines 106 - 138, In clearRules(), ensure
/etc/hosts cleanup isn't skipped when helperClient.isReady is false: either
treat helper unavailability as a failure (return false and set success=false) or
implement a privileged fallback that calls the existing executePrivileged flow
to remove the sentinel host entries; update logic around
helperClient.removeHostsEntries and the else branch that currently only flushes
PF so that removeHostsEntries is invoked (or an equivalent privileged
hosts-cleanup) before returning success in clearRules().
src/bun/proxy/proxy-server.ts (1)

330-352: ⚠️ Potential issue | 🟠 Major

Bedrock EventStream responses are still being dropped.

This path marks AWS EventStream as streaming but replaces the body with "" and never attaches raw chunks, so the persisted trace has no usable response payload and token extraction is skipped. If Bedrock is treated as supported, keep the raw frames/chunks until a real decoder exists.

♻️ Duplicate comments (4)
native/Helper/HelperTool.swift (1)

144-165: ⚠️ Potential issue | 🟠 Major

verifyAnchors still missing anchor allowlist check.

This was flagged in a previous review. Unlike applyPfRules and clearRules, this function doesn't call HelperTool.isAllowedAnchor(anchorName), allowing any authenticated user to query arbitrary PF anchors.

🛡️ Proposed fix
 private func verifyAnchors(id: String, params: [String: Any]) -> [String: Any] {
     guard let anchorName = params["anchorName"] as? String else {
         return ["id": id, "success": false, "error": "Missing anchorName"]
     }
+
+    guard HelperTool.isAllowedAnchor(anchorName) else {
+        return ["id": id, "success": false, "error": "Invalid anchor name"]
+    }

     let rdrResult = run("/sbin/pfctl", args: ["-a", anchorName, "-sn"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/Helper/HelperTool.swift` around lines 144 - 165, verifyAnchors
currently skips the anchor allowlist check; update
HelperTool.verifyAnchors(id:params:) to call
HelperTool.isAllowedAnchor(anchorName) after extracting anchorName and before
running pfctl, returning ["id": id, "success": false, "error": "Not allowed
anchor"] (or similar) when the check fails, and only proceed to run rdr/filter
commands and build the success/data response when isAllowedAnchor(anchorName)
returns true.
src/bun/system/redirect-manager.ts (1)

181-196: ⚠️ Potential issue | 🟠 Major

Preserve the user's existing NODE_EXTRA_CA_CERTS.

These helpers still overwrite the launchd value on enable and always unsetenv it on disable/startup cleanup. That breaks unrelated Node/Bun tooling and makes the rollback path destructive. Save the previous value before setenv and restore it instead of blindly unsetting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/system/redirect-manager.ts` around lines 181 - 196, Change the
helpers so they preserve and restore any existing launchd NODE_EXTRA_CA_CERTS
instead of unconditionally overwriting/unsetting: in setNodeCATrust(certPath)
read the current value via launchctl getenv NODE_EXTRA_CA_CERTS, store that
previous value in a module-scoped variable (e.g., prevNodeExtraCACerts), then
call launchctl setenv with certPath; in clearNodeCATrust() check
prevNodeExtraCACerts—if it is non-empty restore it with launchctl setenv,
otherwise call launchctl unsetenv; keep the existing logging and error handling
but mention restoration vs unsetting.
src/bun/system/pf-manager.ts (1)

141-149: ⚠️ Potential issue | 🟠 Major

Health checks still verify PF only, not DNS interception.

verifyRulesActive() returns healthy on hasRdr alone. If the AgentTap block disappears from /etc/hosts, the watchdog and verifyCapture() still report success while interception is already broken. Verify the helper-managed hosts block alongside the anchor state.

Based on learnings, "DNS-based traffic interception via /etc/hosts + PF rdr on lo0 + DoH upstream resolution + native TCP forwarder with IP_BOUND_IF".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/system/pf-manager.ts` around lines 141 - 149, verifyRulesActive
currently treats hasRdr as sufficient; update it to also verify the
helper-managed hosts (AgentTap block) so DNS interception is checked. When
calling helperClient.verifyAnchors(ANCHOR_NAME) (or a dedicated
helperClient.verifyHosts() if available), require both PF anchor state and the
hosts-block indicator (e.g., result.data?.hasRdr === true &&
result.data?.hasHosts === true) before returning true; if the verifyAnchors
response doesn't include hosts info, add a follow-up helperClient call to
validate the /etc/hosts block presence and include that in the combined boolean
result. Ensure error handling remains but return false when either check fails.
src/bun/storage/trace-db.ts (1)

261-274: ⚠️ Potential issue | 🟠 Major

Expose filtered trace queries, including source app.

The read API still only supports “recent traces”, “trace by id”, and sessions. There is no way to query traces by provider, model, timestamp range, or source app, and getRecentTraces() does not join sessions.source_app at all. Add a filtered trace query surface here and have /traces call it.

As per coding guidelines, "Traces must be queryable by provider, model, timestamp, and source app".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/storage/trace-db.ts` around lines 261 - 274, The current API only
exposes getRecentTraces, getTraceById and getRecentSessions and does not allow
filtering by provider, model, timestamp range or sessions.source_app; add a new
exported function (e.g., getTraces(filters)) that builds a parameterized SQL
query joining traces to sessions (JOIN sessions ON traces.session_id =
sessions.id) to SELECT traces.* plus sessions.source_app and support filters:
provider, model, source_app, timestamp_from, timestamp_to, plus limit/offset;
ensure it uses getDB().query(...).all(...) and safe parameter binding, update
/traces to call this new getTraces function, and either update getRecentTraces
to include sessions.source_app or mark it as a thin wrapper around
getTraces({limit, offset}) so all trace queries can filter by provider, model,
timestamp and source app.
🧹 Nitpick comments (5)
src/bun/dns/doh-resolver.test.ts (1)

32-34: Add a concurrent lookup case for inflight dedupe.

Line 32 and Line 33 are sequential, so this verifies cache reuse but not concurrent request coalescing. Add a Promise.all test for the same domain.

Proposed test addition
 test("resolveViaDoH reuses cached answers for repeated lookups", async () => {
@@
   expect(await resolveViaDoH("api.example.com")).toEqual(["1.2.3.4"]);
   expect(await resolveViaDoH("api.example.com")).toEqual(["1.2.3.4"]);
   expect(calls).toBe(1);
 });
+
+test("resolveViaDoH deduplicates concurrent lookups", async () => {
+  let calls = 0;
+
+  global.fetch = (async (_input: RequestInfo | URL, _init?: RequestInit) => {
+    calls++;
+    await new Promise((r) => setTimeout(r, 10));
+    return new Response(JSON.stringify({
+      Status: 0,
+      Answer: [{ name: "api.example.com", type: 1, TTL: 60, data: "1.2.3.4" }],
+    }), {
+      status: 200,
+      headers: { "content-type": "application/dns-json" },
+    });
+  }) as typeof fetch;
+
+  const [a, b] = await Promise.all([
+    resolveViaDoH("api.example.com"),
+    resolveViaDoH("api.example.com"),
+  ]);
+
+  expect(a).toEqual(["1.2.3.4"]);
+  expect(b).toEqual(["1.2.3.4"]);
+  expect(calls).toBe(1);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/dns/doh-resolver.test.ts` around lines 32 - 34, The test currently
only exercises sequential cache reuse; add a concurrent lookup case to verify
inflight dedupe by invoking two simultaneous resolveViaDoH("api.example.com")
calls with Promise.all and asserting both results equal ["1.2.3.4"] and that the
underlying request counter calls remains 1 (the test uses the calls variable).
Locate the existing expectations around resolveViaDoH and replace or augment the
second sequential expect with a Promise.all([...]) call, then assert the
returned array values and calls === 1 to confirm coalescing.
src/bun/native-bridge.ts (1)

51-56: Directory traversal assumes specific bundle structure.

join(import.meta.dir, "..", "..", "..") works if import.meta.dir is at Contents/Resources/app/.... If the bundle structure changes or code moves deeper, this breaks silently. Consider a more robust approach.

💡 More robust alternative
private getAppContentsDir(): string | null {
  const idx = import.meta.dir.indexOf(".app/Contents/");
  if (idx === -1) return null;
  return import.meta.dir.substring(0, idx + ".app/Contents".length);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/native-bridge.ts` around lines 51 - 56, The current getAppContentsDir
method assumes a fixed depth by using join(import.meta.dir, "..", "..", ".."),
which can break if the bundle layout changes; update getAppContentsDir to locate
the ".app/Contents" substring inside import.meta.dir (e.g., using indexOf or
lastIndexOf) and return the path up through ".app/Contents" (or null if not
found) rather than relying on relative ../ traversal, ensuring you reference the
getAppContentsDir function and import.meta.dir when making the change.
src/bun/storage/trace-db.test.ts (1)

29-43: Consider adding a test for session expiry after gap.

Current tests verify reuse within the gap, but there's no test confirming a new session is created after the gap expires (e.g., timestamp + SESSION_GAP_MS + 1000).

💡 Additional test case
test("getOrCreateSession creates new session after gap expires", () => {
  const provider = `openai-${crypto.randomUUID()}`;
  const timestamp = Date.now();

  const first = getOrCreateSession(provider, timestamp, "Cursor");
  const afterGap = getOrCreateSession(
    provider,
    timestamp + SESSION_GAP_MS + 1000,
    "Cursor",
  );

  expect(afterGap).not.toBe(first);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/storage/trace-db.test.ts` around lines 29 - 43, Add a test that
verifies getOrCreateSession creates a new session when the gap expires: call
getOrCreateSession(provider, timestamp, "Cursor") to get first, then call
getOrCreateSession(provider, timestamp + SESSION_GAP_MS + 1000, "Cursor") and
assert the second result is not the same object as first
(expect(afterGap).not.toBe(first)); use the same provider generation pattern
(e.g., ``openai-${crypto.randomUUID()}``) and Date.now() for timestamp to mirror
existing tests.
native/Forwarder/agenttap-fwd.c (1)

258-265: Fork-per-connection limits scalability.

Each tunnel spawns a new process. Under load with many concurrent API requests, this becomes expensive. Consider a thread pool or async I/O model for production use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/Forwarder/agenttap-fwd.c` around lines 258 - 265, The current
fork-per-connection model (pid = fork() with handle_client(client)) is not
scalable; replace it with a pooled worker thread model or async I/O to avoid
spawning a process per request. Implement a fixed-size pthread pool that accepts
client sockets into a concurrent queue and have worker threads call
handle_client(client) (or convert handle_client to be thread-safe), or refactor
to non-blocking sockets with an event loop (epoll/select) that dispatches
read/write to async handlers; ensure you close descriptors in the parent/worker
appropriately and protect any shared state used by handle_client with mutexes.
native/Helper/HelperTool.swift (1)

304-307: Hardcoded cert name "AgentTap CA" for deletion.

delete-certificate -c "AgentTap CA" assumes a specific common name. If the CA cert's CN differs, deletion silently fails. This is acceptable since the function returns success anyway with a warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/Helper/HelperTool.swift` around lines 304 - 307, The code currently
deletes a hardcoded certificate name via the run(...) call using
"delete-certificate -c 'AgentTap CA'", which can silently fail if the CA CN
differs; update the logic in HelperTool.swift (the function that constructs
`result = run("/usr/bin/security", args: [...])`) to first locate the exact
certificate identifier (SHA-1 or SHA-256 hash) or accept the certificate common
name as a parameter, e.g., call `security find-certificate -a -Z
"/Library/Keychains/System.keychain"` and parse the matching certificate's SHA
hash, then call `security delete-certificate -Z <SHA>` (or use the provided CN
parameter) instead of always using "AgentTap CA", and propagate/report errors
when deletion fails rather than returning success with only a warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@native/Forwarder/agenttap-fwd.c`:
- Line 195: The handshake write call write(client_fd, "OK\n", 3) is not checked
so a failed send could let the code call relay() with a dead client; update the
handshake logic in agenttap-fwd.c to check the return value (and errno) from
write(), handle short/failed writes by closing client_fd and returning/aborting
the path instead of calling relay(), and log an error (or return a non-zero
status) when write returns <= 0; focus changes around the function that performs
the handshake and the subsequent call to relay() so you adjust behavior when
write fails.

In `@src/bun/storage/trace-writer.ts`:
- Around line 58-75: The trace uses different timestamps for session bucketing
and storage: getOrCreateSession(...) is called with completedAt
(trace.request.timestamp + trace.durationMs) but insertTrace(...) writes
timestamp: new Date(trace.request.timestamp).toISOString(), which can make
sessions end before they start; fix by using the same completion timestamp for
both paths—compute completedAtDate = new Date(completedAt).toISOString() (or
pass completedAt as a Date) and use that value for insertTrace.timestamp (or
alternatively change getOrCreateSession to accept the request timestamp and
separately update the session end with completedAt); ensure getOrCreateSession,
insertTrace and any sessions.ended_at update use the same completedAt basis.
- Around line 55-96: The listener in startTraceWriter currently performs
getOrCreateSession and insertTrace synchronously on each "trace" event (blocking
traceEmitter.emit); change it to only enqueue the incoming CapturedTrace into an
in-memory queue and return immediately, and implement a separate async worker
(e.g., a batched flush loop using setImmediate/setInterval or a background
Promise) that dequeues traces, calls getOrCreateSession and insertTrace off the
emit path, and retries/logs errors; reference startTraceWriter, traceEmitter,
CapturedTrace, getOrCreateSession and insertTrace when moving logic so the
listener becomes a fast enqueue-only function and all DB/session work runs in
the background with batching and error handling.

---

Outside diff comments:
In `@src/bun/system/pf-manager.ts`:
- Around line 106-138: In clearRules(), ensure /etc/hosts cleanup isn't skipped
when helperClient.isReady is false: either treat helper unavailability as a
failure (return false and set success=false) or implement a privileged fallback
that calls the existing executePrivileged flow to remove the sentinel host
entries; update logic around helperClient.removeHostsEntries and the else branch
that currently only flushes PF so that removeHostsEntries is invoked (or an
equivalent privileged hosts-cleanup) before returning success in clearRules().

---

Duplicate comments:
In `@native/Helper/HelperTool.swift`:
- Around line 144-165: verifyAnchors currently skips the anchor allowlist check;
update HelperTool.verifyAnchors(id:params:) to call
HelperTool.isAllowedAnchor(anchorName) after extracting anchorName and before
running pfctl, returning ["id": id, "success": false, "error": "Not allowed
anchor"] (or similar) when the check fails, and only proceed to run rdr/filter
commands and build the success/data response when isAllowedAnchor(anchorName)
returns true.

In `@src/bun/storage/trace-db.ts`:
- Around line 261-274: The current API only exposes getRecentTraces,
getTraceById and getRecentSessions and does not allow filtering by provider,
model, timestamp range or sessions.source_app; add a new exported function
(e.g., getTraces(filters)) that builds a parameterized SQL query joining traces
to sessions (JOIN sessions ON traces.session_id = sessions.id) to SELECT
traces.* plus sessions.source_app and support filters: provider, model,
source_app, timestamp_from, timestamp_to, plus limit/offset; ensure it uses
getDB().query(...).all(...) and safe parameter binding, update /traces to call
this new getTraces function, and either update getRecentTraces to include
sessions.source_app or mark it as a thin wrapper around getTraces({limit,
offset}) so all trace queries can filter by provider, model, timestamp and
source app.

In `@src/bun/system/pf-manager.ts`:
- Around line 141-149: verifyRulesActive currently treats hasRdr as sufficient;
update it to also verify the helper-managed hosts (AgentTap block) so DNS
interception is checked. When calling helperClient.verifyAnchors(ANCHOR_NAME)
(or a dedicated helperClient.verifyHosts() if available), require both PF anchor
state and the hosts-block indicator (e.g., result.data?.hasRdr === true &&
result.data?.hasHosts === true) before returning true; if the verifyAnchors
response doesn't include hosts info, add a follow-up helperClient call to
validate the /etc/hosts block presence and include that in the combined boolean
result. Ensure error handling remains but return false when either check fails.

In `@src/bun/system/redirect-manager.ts`:
- Around line 181-196: Change the helpers so they preserve and restore any
existing launchd NODE_EXTRA_CA_CERTS instead of unconditionally
overwriting/unsetting: in setNodeCATrust(certPath) read the current value via
launchctl getenv NODE_EXTRA_CA_CERTS, store that previous value in a
module-scoped variable (e.g., prevNodeExtraCACerts), then call launchctl setenv
with certPath; in clearNodeCATrust() check prevNodeExtraCACerts—if it is
non-empty restore it with launchctl setenv, otherwise call launchctl unsetenv;
keep the existing logging and error handling but mention restoration vs
unsetting.

---

Nitpick comments:
In `@native/Forwarder/agenttap-fwd.c`:
- Around line 258-265: The current fork-per-connection model (pid = fork() with
handle_client(client)) is not scalable; replace it with a pooled worker thread
model or async I/O to avoid spawning a process per request. Implement a
fixed-size pthread pool that accepts client sockets into a concurrent queue and
have worker threads call handle_client(client) (or convert handle_client to be
thread-safe), or refactor to non-blocking sockets with an event loop
(epoll/select) that dispatches read/write to async handlers; ensure you close
descriptors in the parent/worker appropriately and protect any shared state used
by handle_client with mutexes.

In `@native/Helper/HelperTool.swift`:
- Around line 304-307: The code currently deletes a hardcoded certificate name
via the run(...) call using "delete-certificate -c 'AgentTap CA'", which can
silently fail if the CA CN differs; update the logic in HelperTool.swift (the
function that constructs `result = run("/usr/bin/security", args: [...])`) to
first locate the exact certificate identifier (SHA-1 or SHA-256 hash) or accept
the certificate common name as a parameter, e.g., call `security
find-certificate -a -Z "/Library/Keychains/System.keychain"` and parse the
matching certificate's SHA hash, then call `security delete-certificate -Z
<SHA>` (or use the provided CN parameter) instead of always using "AgentTap CA",
and propagate/report errors when deletion fails rather than returning success
with only a warning.

In `@src/bun/dns/doh-resolver.test.ts`:
- Around line 32-34: The test currently only exercises sequential cache reuse;
add a concurrent lookup case to verify inflight dedupe by invoking two
simultaneous resolveViaDoH("api.example.com") calls with Promise.all and
asserting both results equal ["1.2.3.4"] and that the underlying request counter
calls remains 1 (the test uses the calls variable). Locate the existing
expectations around resolveViaDoH and replace or augment the second sequential
expect with a Promise.all([...]) call, then assert the returned array values and
calls === 1 to confirm coalescing.

In `@src/bun/native-bridge.ts`:
- Around line 51-56: The current getAppContentsDir method assumes a fixed depth
by using join(import.meta.dir, "..", "..", ".."), which can break if the bundle
layout changes; update getAppContentsDir to locate the ".app/Contents" substring
inside import.meta.dir (e.g., using indexOf or lastIndexOf) and return the path
up through ".app/Contents" (or null if not found) rather than relying on
relative ../ traversal, ensuring you reference the getAppContentsDir function
and import.meta.dir when making the change.

In `@src/bun/storage/trace-db.test.ts`:
- Around line 29-43: Add a test that verifies getOrCreateSession creates a new
session when the gap expires: call getOrCreateSession(provider, timestamp,
"Cursor") to get first, then call getOrCreateSession(provider, timestamp +
SESSION_GAP_MS + 1000, "Cursor") and assert the second result is not the same
object as first (expect(afterGap).not.toBe(first)); use the same provider
generation pattern (e.g., ``openai-${crypto.randomUUID()}``) and Date.now() for
timestamp to mirror existing tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba0178a8-3201-4108-8ee5-1bb384022bea

📥 Commits

Reviewing files that changed from the base of the PR and between 3d42e30 and 943b6fe.

📒 Files selected for processing (26)
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • AGENTS.md
  • Resources/version.json
  • electrobun.config.ts
  • native/Forwarder/agenttap-fwd.c
  • native/Helper/HelperTool.swift
  • native/Makefile
  • package.json
  • src/bun/ca/ca-manager.ts
  • src/bun/dns/doh-resolver.test.ts
  • src/bun/dns/doh-resolver.ts
  • src/bun/index.ts
  • src/bun/native-bridge.ts
  • src/bun/proxy/proxy-server.ts
  • src/bun/proxy/transparent-proxy.ts
  • src/bun/proxy/upstream-forwarder.ts
  • src/bun/sse/sse-reassembler.test.ts
  • src/bun/sse/sse-reassembler.ts
  • src/bun/storage/trace-db.test.ts
  • src/bun/storage/trace-db.ts
  • src/bun/storage/trace-writer.ts
  • src/bun/system/helper-client.ts
  • src/bun/system/pf-manager.ts
  • src/bun/system/redirect-manager.ts
  • src/shared/types.ts
✅ Files skipped from review due to trivial changes (6)
  • Resources/version.json
  • src/bun/ca/ca-manager.ts
  • package.json
  • src/bun/sse/sse-reassembler.test.ts
  • src/shared/types.ts
  • electrobun.config.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/bun/proxy/transparent-proxy.ts
  • src/bun/dns/doh-resolver.ts
  • src/bun/system/helper-client.ts
  • src/bun/proxy/upstream-forwarder.ts

freeaddrinfo(res);

/* Signal success — client can now start TLS over this tunnel */
write(client_fd, "OK\n", 3);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Check write() return value for handshake response.

If write(client_fd, "OK\n", 3) fails (e.g., client disconnected), the code proceeds to relay() with a potentially broken connection. Should check the return value before continuing.

🛠️ Proposed fix
-    write(client_fd, "OK\n", 3);
+    if (write(client_fd, "OK\n", 3) != 3) {
+        close(upstream);
+        goto done;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/Forwarder/agenttap-fwd.c` at line 195, The handshake write call
write(client_fd, "OK\n", 3) is not checked so a failed send could let the code
call relay() with a dead client; update the handshake logic in agenttap-fwd.c to
check the return value (and errno) from write(), handle short/failed writes by
closing client_fd and returning/aborting the path instead of calling relay(),
and log an error (or return a non-zero status) when write returns <= 0; focus
changes around the function that performs the handshake and the subsequent call
to relay() so you adjust behavior when write fails.

Comment on lines +55 to +96
export function startTraceWriter(): void {
traceEmitter.on("trace", (trace: CapturedTrace) => {
try {
const sourceApp = detectSourceApp(trace.request.headers);
const completedAt = trace.request.timestamp + trace.durationMs;
const safeRequestUrl = sanitizeTraceUrl(trace.request.url);
const requestHeaders = redactHeaders(trace.request.headers);
const responseHeaders = redactHeaders(trace.response.headers);
const sessionId = getOrCreateSession(
trace.provider,
completedAt,
sourceApp,
);

insertTrace({
id: trace.id,
session_id: sessionId,
provider: trace.provider,
domain: trace.domain,
timestamp: new Date(trace.request.timestamp).toISOString(),
model: trace.model ?? null,
request_method: trace.request.method,
request_url: safeRequestUrl,
request_headers: JSON.stringify(requestHeaders),
request_body: trace.request.body,
response_status: trace.response.status,
response_headers: JSON.stringify(responseHeaders),
response_body: trace.response.isStreaming ? null : trace.response.body,
response_chunks: trace.response.chunks ? JSON.stringify(trace.response.chunks) : null,
is_streaming: trace.response.isStreaming ? 1 : 0,
tokens_in: trace.tokensIn ?? null,
tokens_out: trace.tokensOut ?? null,
duration_ms: trace.durationMs,
error: null,
});
} catch (err) {
console.error("[TraceWriter] Failed to write trace:", err);
}
});

console.log("[TraceWriter] Listening for traces");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move trace persistence off the emit path.

This listener does getOrCreateSession() and insertTrace() inline for every trace event. Since traceEmitter.emit() is synchronous, the proxy response path is now blocked on SQLite writes and session aggregate updates. Queue traces and flush them asynchronously instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun/storage/trace-writer.ts` around lines 55 - 96, The listener in
startTraceWriter currently performs getOrCreateSession and insertTrace
synchronously on each "trace" event (blocking traceEmitter.emit); change it to
only enqueue the incoming CapturedTrace into an in-memory queue and return
immediately, and implement a separate async worker (e.g., a batched flush loop
using setImmediate/setInterval or a background Promise) that dequeues traces,
calls getOrCreateSession and insertTrace off the emit path, and retries/logs
errors; reference startTraceWriter, traceEmitter, CapturedTrace,
getOrCreateSession and insertTrace when moving logic so the listener becomes a
fast enqueue-only function and all DB/session work runs in the background with
batching and error handling.

Comment thread src/bun/storage/trace-writer.ts
GeiserX added 2 commits April 15, 2026 19:25
…vailable

When the privileged helper wasn't ready, clearRules() would skip removing
sentinel /etc/hosts entries but still return success=true. This left
127.0.1.x entries that blackhole traffic to intercepted domains.

Now properly reports failure when helper is unavailable and there are
active entries to clean up.
- Move full app state behind auth (new /state endpoint), /status now
  returns only health check info without requiring token
- Remove double-await dead code in privilege-executor.ts
- Clear activeSessions/insertStmt on DB close to prevent stale state
- Use completion timestamp (not request start) for trace rows to match
  session bucketing
- Remove dead dns-cache.ts (superseded by DoH resolver's built-in cache)
- Remove unused getRecentTraces export
@GeiserX GeiserX merged commit a2cdc12 into main Apr 15, 2026
6 checks 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.

1 participant