Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fa30740
fix(cli): verify better-sqlite3 native binary is actually loadable (p…
diegosouzapw Jul 14, 2026
76b3820
fix(executors): forward X-Session-ID/X-Title agent metadata headers (…
diegosouzapw Jul 14, 2026
7335557
fix(sse): sanitize non-ok Antigravity streaming error body (port from…
diegosouzapw Jul 14, 2026
b17eaed
chore(changelog): move #2413 entry to changelog.d fragment
diegosouzapw Jul 14, 2026
fb0e418
chore(changelog): move #2493 entry to changelog.d fragment
diegosouzapw Jul 14, 2026
8ce4ac5
fix(sse): handle space-separated arg name/value in Composer tool call…
diegosouzapw Jul 14, 2026
88c2e1a
fix(cli): remove MITM DNS spoof entries before killing server process…
diegosouzapw Jul 14, 2026
6eef682
refactor(mitm): extract repair planning out of manager to respect the…
diegosouzapw Jul 15, 2026
3de7b97
fix(mitm): split stopMitm() DNS/kill steps to fix complexity ratchet …
diegosouzapw Jul 15, 2026
361b39a
Merge branch 'release/v3.8.49' into fix/port-issue-2413-preserve-agen…
diegosouzapw Jul 16, 2026
d576a08
Merge branch 'release/v3.8.49' into fix/port-issue-2493-abi-validation
diegosouzapw Jul 16, 2026
ca6aa42
Merge branch 'release/v3.8.49' into fix/port-issue-2461-antigravity-4…
diegosouzapw Jul 16, 2026
7d0f749
Merge branch 'release/v3.8.49' into fix/port-issue-1811-composer-spac…
diegosouzapw Jul 16, 2026
f89cefc
Merge branch 'release/v3.8.49' into fix/port-issue-1809-mitm-dns-order
diegosouzapw Jul 16, 2026
c90c60b
Merge of #7104
mergify[bot] Jul 16, 2026
6ecdfdd
Merge of #7105
mergify[bot] Jul 16, 2026
ae39b86
Merge of #7106
mergify[bot] Jul 16, 2026
1550494
Merge of #7116
mergify[bot] Jul 16, 2026
22bb6a7
Merge of #7117
mergify[bot] Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions bin/cli/runtime/nativeDeps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ export function hasModule(name) {
return existsSync(join(runtimeModules(), name, "package.json"));
}

/**
* Probe whether a native addon (.node) file can actually be dlopen'd by the Node runtime that
* is going to load it. Runs in a throwaway subprocess so a real ABI mismatch (which can segfault
* the process instead of throwing) never takes down the caller — only the probe subprocess.
*/
function probeNativeBinaryLoadable(binary) {
try {
const res = spawnSync(
process.execPath,
[
"-e",
"try { require(process.argv[1]); process.exit(0); } catch (e) { process.exit(1); }",
binary,
],
{ timeout: 10_000, stdio: "ignore" }
);
// status === 0 means require() (and therefore dlopen) succeeded. Anything else — a thrown
// ERR_DLOPEN_FAILED/NODE_MODULE_VERSION mismatch (status 1) or a crash (status null with a
// signal, e.g. SIGSEGV) — means the binary is not safe to load.
return res.status === 0;
} catch {
return false;
}
}

export function isBetterSqliteBinaryValid() {
const binary = join(
runtimeModules(),
Expand All @@ -68,10 +93,18 @@ export function isBetterSqliteBinaryValid() {
closeSync(fd);
const magic = buf.toString("hex");
const os = platform();
if (os === "linux") return magic.startsWith("7f454c46"); // ELF
if (os === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
if (os === "win32") return magic.startsWith("4d5a"); // PE/MZ
return true;
let formatOk;
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
else if (os === "darwin")
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
else formatOk = true;
if (!formatOk) return false;
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI
// (NODE_MODULE_VERSION) that will load it — a stale/foreign-ABI binary passes the header
// check and then crashes (segfault) on load instead of triggering a rebuild. Actually
// attempt to load it, isolated in a subprocess.
return probeNativeBinaryLoadable(binary);
} catch {
return false;
}
Expand Down
1 change: 1 addition & 0 deletions changelog.d/fixes/1809-mitm-stop-dns-before-kill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(cli):** `stopMitm()` now removes /etc/hosts DNS-spoof entries before killing the MITM server process, closing the window where a client's DNS still resolved a target host to `127.0.0.1` while nothing was listening there — the cause of `connect ECONNREFUSED 127.0.0.1:443` right after stopping the MITM proxy (thanks @dionisius95).
1 change: 1 addition & 0 deletions changelog.d/fixes/1811-composer-space-sep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(sse):** Cursor Composer/Auto tool calls that separate the arg name and value with a space instead of a newline (e.g. `path /Users/.../test`) no longer produce empty-valued, malformed argument keys, fixing silent no-op Write/tool calls. (thanks @way-art)
1 change: 1 addition & 0 deletions changelog.d/fixes/2413-preserve-agent-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(executors):** forward agent-supplied `X-Session-ID`/`X-Title` metadata headers to upstream providers — previously dropped for every client outside the `x-opencode-*` allowlist. (thanks @chitholian) (#7104)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(sse):** Antigravity streaming requests that hit a non-ok upstream response (e.g. a 403) no longer pipe the raw upstream bytes straight through to the client — a binary/non-UTF8 error body (observed as gzip-magic-byte garbage) is now routed through the same sanitized `buildAntigravityUpstreamError()` path the non-streaming branch already used, instead of corrupting the client-visible error message. Regression guard: `tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts` — thanks @Duongkhanhtool
1 change: 1 addition & 0 deletions changelog.d/fixes/2493-better-sqlite3-abi-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(cli):** the runtime self-heal now verifies a cached `better-sqlite3` native binary actually loads for the running Node before trusting it — the old check only inspected the file's magic bytes (ELF/Mach-O/PE header), so a binary built for a different Node ABI passed validation and segfaulted the process on first use instead of triggering a rebuild. (thanks @mrprohack) (#7105)
28 changes: 28 additions & 0 deletions open-sse/executors/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,34 @@ export class AntigravityExecutor extends BaseExecutor {
};
}

// #2461: a non-ok upstream response (e.g. 403) must never be piped through the
// streaming pass-through below as if it were an SSE body. Google occasionally
// returns non-UTF8/binary error bodies (observed: gzip-magic-byte payloads) for
// 403s on this endpoint; reading/forwarding those raw bytes corrupts the
// client-visible error message. Mirror the non-streaming branch above and build
// a sanitized JSON error via buildAntigravityUpstreamError (hard rule #12)
// instead of streaming unknown bytes straight through.
if (!response.ok) {
const rawBody = await response
.clone()
.text()
.catch(() => "");
const errorBody = buildAntigravityUpstreamError(
response.status,
response.statusText,
rawBody
);
return {
response: new Response(JSON.stringify(errorBody), {
status: response.status,
headers: { "Content-Type": "application/json" },
}),
url,
headers: finalHeaders,
transformedBody: attachToolNameMap(transformedBody, requestToolNameMap),
};
}

// Streaming path: wrap the response body in a pass-through TransformStream
// that extracts remainingCredits from the final SSE chunk(s) without
// consuming the stream. The client receives the unmodified SSE data.
Expand Down
23 changes: 18 additions & 5 deletions open-sse/utils/composerToolCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,28 @@ function parseInnerCall(body: string): { name: string; arguments: string } | nul
const args: Record<string, unknown> = {};
for (const seg of segments) {
if (!seg) continue;
// Each segment is `arg_name\nvalue\n...`. The arg name is the first
// line; everything after the first newline is the value (verbatim,
// including additional newlines).
// Each segment is normally `arg_name\nvalue\n...`: the arg name is the
// first line, everything after the first newline is the value
// (verbatim, including additional newlines). Some live Composer/Auto
// captures instead separate the arg name and value with a single space
// on the same line (no newline at all in the segment) — fall back to
// splitting on the first whitespace boundary in that case so the value
// isn't swallowed into an empty-valued, space-containing "arg name".
const idxNl = seg.indexOf("\n");
let argName: string;
let argValue: string;
if (idxNl < 0) {
argName = seg.trim();
argValue = "";
const idxSp = seg.search(/\s/);
if (idxSp < 0) {
argName = seg.trim();
argValue = "";
} else {
argName = seg.slice(0, idxSp).trim();
// Unlike the newline-delimited form, a space-delimited value has no
// multi-line content to preserve — trim the trailing whitespace left
// over from the boundary with the next `<|tool▁sep|>` marker.
argValue = seg.slice(idxSp + 1).trim();
}
} else {
argName = seg.slice(0, idxNl).trim();
argValue = seg.slice(idxNl + 1);
Expand Down
19 changes: 19 additions & 0 deletions open-sse/utils/opencodeHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ const OPENCODE_HEADER_KEYS = [
"x-opencode-client",
] as const;

/**
* Common agent-metadata headers used by non-OpenCode clients (custom agents/
* providers) for upstream request tracking and attribution. Forwarded the same
* way as the x-opencode-* set: case-insensitive lookup, client value wins.
* Added for 9router#2413 — these were previously dropped for every client
* outside the OpenCode allowlist.
*/
const AGENT_METADATA_HEADER_KEYS = ["x-session-id", "x-title"] as const;

/**
* Case-insensitive lookup for a header in a headers record.
*/
Expand All @@ -26,6 +35,8 @@ function findHeader(headers: Record<string, string>, name: string): string | und
* 1. Forwards User-Agent from clientHeaders via `setUserAgentHeader()`
* 2. Forwards x-opencode-session, x-opencode-request, x-opencode-project,
* x-opencode-client headers (case-insensitive match)
* 3. Forwards x-session-id, x-title agent-metadata headers (case-insensitive
* match) — common conventions used by non-OpenCode agent clients (9router#2413)
*
* @param headers - The outbound headers record to mutate
* @param clientHeaders - The client-provided headers to forward from
Expand Down Expand Up @@ -60,6 +71,14 @@ export function forwardOpencodeClientHeaders(
}
}

// 2b. Forward agent-metadata headers (x-session-id, x-title) — 9router#2413
for (const headerName of AGENT_METADATA_HEADER_KEYS) {
const value = findHeader(clientHeaders, headerName);
if (value) {
headers[headerName] = value;
}
}

// 3. OpencodeExecutor-only: synthesize session/request id from fallback headers
if (options?.synthesizeRequestId && !headers["x-opencode-session"]) {
const sessionAffinity =
Expand Down
Loading
Loading