From 14bb9b323dafcd9ff3528a588cac2a0fb19aeb23 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 14:38:41 -0400 Subject: [PATCH 01/21] fix(ci): build with Bun 1.4.4 so release binaries drop the eMudhra root Bun 1.3.14 embeds "emSign ECC Root CA - G3", one of the roots the compromised eMudhra intermediates chain to, and `bun build --compile` copies that root list into every release binary. Bun 1.4.4 ships the NSS 3.131.1 root list without it and adds Node's blocked public keys check (oven-sh/bun#46300). Moves every workflow pin from 1.3.14 to 1.4.4, including the DAST target image (oven/bun:1.4.4-slim, index digest sha256:1846adf2...87d9a4, re-read from the registry), and updates the Renovate pin note and the build doc. Taken in-house from #2214. Co-authored-by: iMacTia <2527520+iMacTia@users.noreply.github.com> --- .github/renovate.json | 2 +- .github/workflows/dast.yml | 4 ++-- .github/workflows/deploy.yml | 8 ++++---- .github/workflows/guides-show-deploy.yml | 2 +- .github/workflows/inbox-claude-code.yml | 2 +- .github/workflows/inbox-e2e.yml | 2 +- .github/workflows/inbox-pi.yml | 2 +- .github/workflows/relay.yml | 2 +- .github/workflows/release.yml | 10 +++++----- .github/workflows/security.yml | 2 +- .github/workflows/test.yml | 10 +++++----- docs/agent-guides/project-and-development.md | 2 +- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 92232c51bd..8799cf9f42 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -15,7 +15,7 @@ "automerge": false }, { - "description": "Pin Bun to 1.3.14: 1.3.12 broke macOS binary codesign for bun build --compile (hotfixed in v0.17.9); 1.3.14 fixes it and is the version every workflow builds with (#1250). Unpin only after verifying signed builds work on the target version.", + "description": "Pin Bun to 1.4.4: the first release whose bundled root certificates drop the compromised eMudhra roots (NSS 3.131.1, oven-sh/bun#46300), and the version every workflow builds with. 1.3.12 broke macOS binary codesign for bun build --compile (hotfixed in v0.17.9). Unpin only after verifying signed builds work on the target version.", "matchManagers": ["github-actions"], "matchDepNames": ["oven-sh/setup-bun"], "matchDepTypes": ["action"], diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml index 4463dfc557..d01669bbbf 100644 --- a/.github/workflows/dast.yml +++ b/.github/workflows/dast.yml @@ -13,8 +13,8 @@ concurrency: cancel-in-progress: false env: - BUN_VERSION: 1.3.14 - TARGET_IMAGE: oven/bun:1.3.14-slim@sha256:d56a2534ffd262e92c12fd3249d3924d296d97086da773f821d7d0477435ea04 + BUN_VERSION: 1.4.4 + TARGET_IMAGE: oven/bun:1.4.4-slim@sha256:1846adf22496bd711036aae8be147e57bdebbaea2e5264e3e0b5af1db187d9a4 ZAP_VERSION: 2.17.0 ZAP_IMAGE: ghcr.io/zaproxy/zaproxy:stable@sha256:781a2bdaea47324e7bab583e2263f21d257b0aee61ed51521a5be45f5f5081ef DAST_NETWORK: plannotator-dast-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2feaad4523..bd3c94e73a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -188,7 +188,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -245,7 +245,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -282,7 +282,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -308,7 +308,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/guides-show-deploy.yml b/.github/workflows/guides-show-deploy.yml index e8db8242d0..ef0d4f9e80 100644 --- a/.github/workflows/guides-show-deploy.yml +++ b/.github/workflows/guides-show-deploy.yml @@ -29,7 +29,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - name: Install dependencies diff --git a/.github/workflows/inbox-claude-code.yml b/.github/workflows/inbox-claude-code.yml index e8d10cdff3..80186fefa9 100644 --- a/.github/workflows/inbox-claude-code.yml +++ b/.github/workflows/inbox-claude-code.yml @@ -67,7 +67,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/inbox-e2e.yml b/.github/workflows/inbox-e2e.yml index aa4cc57b8f..1abad11fec 100644 --- a/.github/workflows/inbox-e2e.yml +++ b/.github/workflows/inbox-e2e.yml @@ -128,7 +128,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/inbox-pi.yml b/.github/workflows/inbox-pi.yml index 7e9c4defac..10312cf8ef 100644 --- a/.github/workflows/inbox-pi.yml +++ b/.github/workflows/inbox-pi.yml @@ -69,7 +69,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 # Pi needs Node 22.19+ (its engines field). - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/relay.yml b/.github/workflows/relay.yml index 02713f4c02..164b082266 100644 --- a/.github/workflows/relay.yml +++ b/.github/workflows/relay.yml @@ -74,7 +74,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0a9669f08..3fea43ead1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - name: Install dependencies @@ -92,7 +92,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - name: Install dependencies @@ -867,7 +867,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -1199,7 +1199,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -1257,7 +1257,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 no-cache: true - name: Download binary subjects diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index fb9478489c..112d47c217 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -615,7 +615,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Materialize packages for license evidence shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dae42f94f8..5432f45a72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -479,7 +479,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -590,7 +590,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 if: steps.gate.outputs.run == 'true' with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies if: steps.gate.outputs.run == 'true' @@ -645,7 +645,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -667,7 +667,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.3.14 + bun-version: 1.4.4 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: diff --git a/docs/agent-guides/project-and-development.md b/docs/agent-guides/project-and-development.md index 80f6752b0a..84e7cb2e2c 100644 --- a/docs/agent-guides/project-and-development.md +++ b/docs/agent-guides/project-and-development.md @@ -214,7 +214,7 @@ bun run --cwd apps/review build && bun run build:hook && \ --define "__CLI_VERSION__=\"0.0.0-dev\"" --outfile ~/.local/bin/plannotator ``` -These are the flags `.github/workflows/release.yml` compiles with; without the `__CLI_VERSION__` define `plannotator --version` falls back to `plannotator dev` and the binary cannot be told apart from any other local build. On macOS build with bun >= 1.3.14 (what CI pins): 1.3.12 loses the linker signature and the compiled binary is SIGKILLed on launch with no error (#541). +These are the flags `.github/workflows/release.yml` compiles with; without the `__CLI_VERSION__` define `plannotator --version` falls back to `plannotator dev` and the binary cannot be told apart from any other local build. On macOS build with bun >= 1.3.14 (CI pins 1.4.4): 1.3.12 loses the linker signature and the compiled binary is SIGKILLed on launch with no error (#541). Running only `build:opencode` will copy stale HTML files. From 420e16a4e86f4c1c849027cb7e12ae88d44f98e2 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 14:38:51 -0400 Subject: [PATCH 02/21] test: count node:http listeners from Bun 1.4's baseline; wait for written files Bun 1.4, like Node, gives every node:http Server its own "listening" listener at construction (connection tracking), so the Pi port tests assert that listenOnPort leaves no listener of its own rather than that there are none. Fixes three failures on 1.4.4. waitForFile (agent-terminal and the Pi browser-launch tests) now waits for content: a fake program's `> file` creates the file before printf writes it, and a read in between saw it empty (seen once on 1.4.4). From #1489. Co-authored-by: FNDEVVE <141281206+FNDEVVE@users.noreply.github.com> --- apps/pi-extension/server/network.test.ts | 16 ++++++++++++---- packages/server/agent-terminal.test.ts | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/pi-extension/server/network.test.ts b/apps/pi-extension/server/network.test.ts index be574b7eed..c5f7351121 100644 --- a/apps/pi-extension/server/network.test.ts +++ b/apps/pi-extension/server/network.test.ts @@ -5,6 +5,7 @@ import { mkdtempSync, readFileSync, rmSync, + statSync, writeFileSync, } from "node:fs"; import { createServer } from "node:http"; @@ -169,13 +170,16 @@ describe("pi port selection", () => { await closeServer(servers[1]); process.env.PLANNOTATOR_PORT = `${start}-${start + 1}`; const server = createServer(); + // Bun >= 1.4, like Node, gives every node:http Server its own "listening" + // listener (connection tracking): count only what listenOnPort leaves. + const initialListening = server.listenerCount("listening"); try { expect(await listenOnPort(server)).toEqual({ port: start + 1, portSource: "env", }); expect(server.listenerCount("error")).toBe(0); - expect(server.listenerCount("listening")).toBe(0); + expect(server.listenerCount("listening")).toBe(initialListening); } finally { await closeServer(server); await closeServer(servers[0]); @@ -217,11 +221,12 @@ describe("pi port selection", () => { const { start, servers } = await occupyConsecutivePorts(12); process.env.PLANNOTATOR_PORT = `${start}-${start + servers.length - 1}`; const server = createServer(); + const initialListening = server.listenerCount("listening"); try { await expect(listenOnPort(server)).rejects.toThrow("exhausted"); expect(server.listenerCount("error")).toBe(0); - expect(server.listenerCount("listening")).toBe(0); + expect(server.listenerCount("listening")).toBe(initialListening); } finally { await Promise.all(servers.map(closeServer)); } @@ -234,13 +239,14 @@ describe("pi non-range port compatibility", () => { const { start, servers } = await occupyConsecutivePorts(1); process.env.PLANNOTATOR_PORT = String(start); const server = createServer(); + const initialListening = server.listenerCount("listening"); try { await expect(listenOnPort(server)).rejects.toThrow( new RegExp(`^Port ${start} in use after 5 retries$`), ); expect(server.listenerCount("error")).toBe(0); - expect(server.listenerCount("listening")).toBe(0); + expect(server.listenerCount("listening")).toBe(initialListening); } finally { await closeServer(servers[0]); } @@ -402,9 +408,11 @@ function writeExecutable(dir: string, name: string, body: string): string { return file; } +// The fake browser's `> file` creates the file before printf writes to it: +// wait for content, not just the file, or the read can see it empty. async function waitForFile(file: string, timeoutMs = 2000): Promise { const deadline = Date.now() + timeoutMs; - while (!existsSync(file)) { + while (!existsSync(file) || statSync(file).size === 0) { if (Date.now() > deadline) throw new Error(`timed out waiting for ${file}`); await new Promise((resolve) => setTimeout(resolve, 10)); } diff --git a/packages/server/agent-terminal.test.ts b/packages/server/agent-terminal.test.ts index ea8dd01614..e73abe3748 100644 --- a/packages/server/agent-terminal.test.ts +++ b/packages/server/agent-terminal.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -206,7 +206,8 @@ export function createNodePtyWebSocketServer(options) { async function waitForFile(path: string): Promise { const started = Date.now(); while (Date.now() - started < 5_000) { - if (existsSync(path)) return; + // Created is not written: wait for content, so a read never sees it empty. + if (existsSync(path) && statSync(path).size > 0) return; await new Promise((resolve) => setTimeout(resolve, 25)); } throw new Error(`Timed out waiting for ${path}`); From b3cd1951f24d25578a79fa91aa9ced9f0a217e0a Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 14:38:51 -0400 Subject: [PATCH 03/21] fix(pi): end open event streams before stop() destroys their sockets closeKeepingDraft() sends the open tab session-closed and Pi calls stop() right after; stop()'s closeAllConnections() destroys the socket and drops that write. On Node 24 (Pi's runtime) and Bun 1.4 the tab never learns the review closed; Bun 1.3's closeAllConnections() left the stream open, which is why decision-listener-throw.test.ts passed. stop() now ends every open stream first, which sends it (checked on Node 24, Bun 1.3.14 and 1.4.4). Plan, annotate and review servers. --- .../pi-extension/server/external-annotations.ts | 17 +++++++++++++++++ apps/pi-extension/server/serverAnnotate.ts | 2 ++ apps/pi-extension/server/serverPlan.ts | 2 ++ apps/pi-extension/server/serverReview.ts | 2 ++ 4 files changed, 23 insertions(+) diff --git a/apps/pi-extension/server/external-annotations.ts b/apps/pi-extension/server/external-annotations.ts index a7b9ca7f7d..2bb71d0ecc 100644 --- a/apps/pi-extension/server/external-annotations.ts +++ b/apps/pi-extension/server/external-annotations.ts @@ -56,6 +56,23 @@ export function createExternalAnnotationHandler(mode: "plan" | "review") { /** Send an event that is not a store mutation (session-closed) to every open stream. */ broadcast, + /** + * End every open stream. A server's stop() calls this before + * closeAllConnections(): that destroys the sockets, and on Node (and Bun + * >= 1.4) a destroy drops what was just written, so the tab never got the + * session-closed sent right before stop(). Ending first sends it. + */ + endStreams() { + for (const res of subscribers) { + try { + res.end(); + } catch { + // Already closed. + } + } + subscribers.clear(); + }, + /** Push annotations directly into the store (bypasses HTTP, reuses same validation). */ addAnnotations(body: unknown): { ids: string[] } | { error: string } { const parsed = transform(body); diff --git a/apps/pi-extension/server/serverAnnotate.ts b/apps/pi-extension/server/serverAnnotate.ts index 023c6b3b25..a029430fe3 100644 --- a/apps/pi-extension/server/serverAnnotate.ts +++ b/apps/pi-extension/server/serverAnnotate.ts @@ -1568,6 +1568,8 @@ export async function startAnnotateServer(options: { } } } finally { + // Before closeAllConnections, or a session-closed just sent is lost. + externalAnnotations.endStreams(); server.close(); // close() only stops the listener; drain browser keep-alive sockets so a // stopped session's connections die immediately instead of at the diff --git a/apps/pi-extension/server/serverPlan.ts b/apps/pi-extension/server/serverPlan.ts index 11e2b2f456..636e486188 100644 --- a/apps/pi-extension/server/serverPlan.ts +++ b/apps/pi-extension/server/serverPlan.ts @@ -773,6 +773,8 @@ export async function startPlanReviewServer(options: { closeAllFileBrowserWatchers(); aiRuntime?.dispose(); } finally { + // Before closeAllConnections, or a session-closed just sent is lost. + externalAnnotations?.endStreams(); server.close(); // close() only stops the listener; drain browser keep-alive sockets so a // stopped session's connections die immediately instead of at the diff --git a/apps/pi-extension/server/serverReview.ts b/apps/pi-extension/server/serverReview.ts index 65c83ab827..74422c0861 100644 --- a/apps/pi-extension/server/serverReview.ts +++ b/apps/pi-extension/server/serverReview.ts @@ -4060,6 +4060,8 @@ export async function startReviewServer(options: { callFlowService.cancelAll(); aiRuntime?.dispose(); } finally { + // Before closeAllConnections, or a session-closed just sent is lost. + externalAnnotations.endStreams(); server.close(); // close() only stops the listener; drain browser keep-alive sockets so a // stopped session's connections die immediately instead of at the From f5baf928dd52877af393e07eb44cca2f37cbf7eb Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 14:38:51 -0400 Subject: [PATCH 04/21] fix(ai): keep the OpenCode SDK's stream cancel from ending the process on Bun 1.4 The SDK's SSE abort handler calls `void reader.cancel()`. On Bun 1.4 the aborted fetch has already errored the body, so cancel() rejects with AbortError, unhandled, and an unhandled rejection ends the process: the compiled server on every OpenCode Ask AI answer or Stop. Node 24 and Bun 1.3.14 resolve it. @opencode-ai/sdk 1.19.4 has the same code. patches/@opencode-ai%2Fsdk@1.19.3.patch catches it (v1 and v2 clients). The "through the real client" tests in opencode-sdk.test.ts fail on 1.4.4 without it. bun.lock only gains the patchedDependencies entry. --- bun.lock | 3 ++ docs/agent-guides/project-and-development.md | 2 ++ package.json | 3 ++ patches/@opencode-ai%2Fsdk@1.19.3.patch | 30 ++++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 patches/@opencode-ai%2Fsdk@1.19.3.patch diff --git a/bun.lock b/bun.lock index b1b33ee43d..cff8c01131 100644 --- a/bun.lock +++ b/bun.lock @@ -451,6 +451,9 @@ }, }, }, + "patchedDependencies": { + "@opencode-ai/sdk@1.19.3": "patches/@opencode-ai%2Fsdk@1.19.3.patch", + }, "overrides": { "dompurify": "^3.4.17", "lodash-es": "4.18.1", diff --git a/docs/agent-guides/project-and-development.md b/docs/agent-guides/project-and-development.md index 84e7cb2e2c..0b86a7a290 100644 --- a/docs/agent-guides/project-and-development.md +++ b/docs/agent-guides/project-and-development.md @@ -216,6 +216,8 @@ bun run --cwd apps/review build && bun run build:hook && \ These are the flags `.github/workflows/release.yml` compiles with; without the `__CLI_VERSION__` define `plannotator --version` falls back to `plannotator dev` and the binary cannot be told apart from any other local build. On macOS build with bun >= 1.3.14 (CI pins 1.4.4): 1.3.12 loses the linker signature and the compiled binary is SIGKILLed on launch with no error (#541). +**Patched dependency.** `patches/@opencode-ai%2Fsdk@1.19.3.patch` (root `patchedDependencies`) makes the SDK's SSE abort handler catch `reader.cancel()`. On Bun 1.4 that promise rejects once the aborted fetch has errored the body, and the unhandled rejection ends the process: every OpenCode Ask AI answer or Stop aborts the event stream. `packages/ai/providers/opencode-sdk.test.ts` ("through the real client") fails without it. A new SDK version needs the patch carried over (`bun patch @opencode-ai/sdk`) until upstream catches the promise. + Running only `build:opencode` will copy stale HTML files. ## Marketing Site diff --git a/package.json b/package.json index 4193a7a129..bc1716c2fa 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,8 @@ "sharp": "0.35.5", "source-map-js": "1.2.2", "proxy-addr": "2.0.8" + }, + "patchedDependencies": { + "@opencode-ai/sdk@1.19.3": "patches/@opencode-ai%2Fsdk@1.19.3.patch" } } diff --git a/patches/@opencode-ai%2Fsdk@1.19.3.patch b/patches/@opencode-ai%2Fsdk@1.19.3.patch new file mode 100644 index 0000000000..b9ea181ecf --- /dev/null +++ b/patches/@opencode-ai%2Fsdk@1.19.3.patch @@ -0,0 +1,30 @@ +diff --git a/dist/gen/core/serverSentEvents.gen.js b/dist/gen/core/serverSentEvents.gen.js +index 51b0e275621e9140e0307ca80de29fdf23c925b1..3e7a6fbf20c8773e47f2fe2fa4f24ccdd53f0b2b 100644 +--- a/dist/gen/core/serverSentEvents.gen.js ++++ b/dist/gen/core/serverSentEvents.gen.js +@@ -26,7 +26,9 @@ export const createSseClient = ({ onSseError, onSseEvent, responseTransformer, r + let buffer = ""; + const abortHandler = () => { + try { +- void reader.cancel(); ++ // Bun >= 1.4 rejects this once the aborted fetch has errored the body; ++ // unobserved, that rejection ends the process. ++ reader.cancel().catch(() => { }); + } + catch { + // noop +diff --git a/dist/v2/gen/core/serverSentEvents.gen.js b/dist/v2/gen/core/serverSentEvents.gen.js +index 9684cb4f19593266b94f78e933f5039dc362c5a9..47302c680ab8cccf10dd292e3460c0dfc708ae58 100644 +--- a/dist/v2/gen/core/serverSentEvents.gen.js ++++ b/dist/v2/gen/core/serverSentEvents.gen.js +@@ -40,7 +40,9 @@ export const createSseClient = ({ onRequest, onSseError, onSseEvent, responseTra + let buffer = ""; + const abortHandler = () => { + try { +- reader.cancel(); ++ // Bun >= 1.4 rejects this once the aborted fetch has errored the body; ++ // unobserved, that rejection ends the process. ++ reader.cancel().catch(() => { }); + } + catch { + // noop From 8e92f05f9a12e32e7a1360dcda727c34f10847bb Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:19:12 -0400 Subject: [PATCH 05/21] fix: two DOM checks Bun 1.3 passed while they failed Bun 1.3.14's expect(x).toBeNull() passed for an element too big to print (a React-attached node), so two DOM checks were green while false. Bun 1.4.4 reports them. - DiagramViewer: a host that takes commenting away (no handler, no reason) left the open composer up as a Comment form nothing could save. It now closes, as it does when the source pane closes. - Inbox compose: the seeded world has an unsent reply (a picked question), so the Drafts row is right to show 1 after Discard. The check now asserts the row counts only those replies. --- packages/inbox/compose.test.tsx | 6 +++++- packages/ui/components/diagram/DiagramViewer.tsx | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/inbox/compose.test.tsx b/packages/inbox/compose.test.tsx index d9baca0a6f..93a35622ff 100644 --- a/packages/inbox/compose.test.tsx +++ b/packages/inbox/compose.test.tsx @@ -222,7 +222,11 @@ test.skipIf(!hasDom)('no live agent: Send sends nothing and says so; Escape asks act(() => (dialog()!.querySelector('[data-compose-discard]') as HTMLButtonElement).click()); await until(() => dialog() === null, 'compose closed'); expect(await drafts()).toEqual([]); - expect(el.querySelector('[data-drafts-nav]')).toBeNull(); + // The Drafts row counts only the seeded world's unsent replies (a picked + // question waits there), never the discarded message. Bun 1.3 passed a + // failing `.toBeNull()` here when the element was too big to print. + const { replies } = JSON.parse((await loopback('/api/inbox/drafts')).text) as { replies?: unknown[] }; + expect(el.querySelector('[data-drafts-nav] .ib-n')?.textContent ?? '0').toBe(String(replies?.length ?? 0)); }); test.skipIf(!hasDom)('Keep draft: the sidebar\'s Drafts row and the Drafts page list it with its chip; delete has Undo', async () => { diff --git a/packages/ui/components/diagram/DiagramViewer.tsx b/packages/ui/components/diagram/DiagramViewer.tsx index 146bfb7455..d61b9e0df0 100644 --- a/packages/ui/components/diagram/DiagramViewer.tsx +++ b/packages/ui/components/diagram/DiagramViewer.tsx @@ -204,6 +204,12 @@ export function DiagramViewer({ if (!sourceOpen) cancelComposer(); }, [cancelComposer, sourceOpen]); + // A host that takes commenting away (no handler, no reason) closes an open + // composer: left open it became a Comment form nothing could save. + useEffect(() => { + if (!canCreate) cancelComposer(); + }, [cancelComposer, canCreate]); + // The composer handed to the host: the draft's part goes out, and the // host's cancel closes it only while it is still the open one. const onHostDraftRef = useRef(onHostDraft); From ee3b7d6083cb7a38bf939d3db1195913126ba484 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:22:14 -0400 Subject: [PATCH 06/21] ci(release): gate every binary on its Bun runtime's trust After compiling, before any upload: every release binary, paste binary and the baseline control must embed Bun >= 1.4.4, carry the blocked-key check (oven-sh/bun#46300; its test hook addBlockedSPKIForTesting is the marker, the eight key hashes are folded into code), and hold no emSign ECC Root CA - G3 as PEM, DER or public key. scripts/check-bun-runtime-trust.ts reads bytes only, so it checks the darwin and Windows binaries on the Linux builder too. Checked locally: all six 1.4.4 targets pass; 1.3.14 CLI and paste builds fail on all three counts. --- .github/workflows/release.yml | 18 ++++ scripts/check-bun-runtime-trust.test.ts | 52 +++++++++++ scripts/check-bun-runtime-trust.ts | 114 ++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 scripts/check-bun-runtime-trust.test.ts create mode 100644 scripts/check-bun-runtime-trust.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fea43ead1..c72a9597aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -174,6 +174,24 @@ jobs: mkdir -p baseline-control bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig --target=bun-linux-x64 --define "__CLI_VERSION__=\"$VERSION\"" --outfile baseline-control/control-cli-linux-x64-default + # Every binary carries the Bun runtime it was compiled with, root + # certificates included. Fail unless each one embeds Bun >= 1.4.4, has + # the blocked-key check, and holds no emSign ECC Root CA - G3 (the + # compromised eMudhra chain, Oct 2026). Before any upload. + - name: Gate - Bun runtime trust in every binary + run: | + set -euo pipefail + binaries=() + for f in plannotator-* baseline-control/control-cli-linux-x64-default; do + [[ "$f" == *.sha256 ]] || binaries+=("$f") + done + # 6 CLI + 6 paste targets + the control: never a gate over nothing. + if (( ${#binaries[@]} < 13 )); then + echo "FAIL: expected at least 13 binaries, found ${#binaries[@]}" >&2 + exit 1 + fi + bun scripts/check-bun-runtime-trust.ts "${binaries[@]}" + - name: Upload baseline-gate control binary uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/scripts/check-bun-runtime-trust.test.ts b/scripts/check-bun-runtime-trust.test.ts new file mode 100644 index 0000000000..651a818c62 --- /dev/null +++ b/scripts/check-bun-runtime-trust.test.ts @@ -0,0 +1,52 @@ +/** + * The release gate in scripts/check-bun-runtime-trust.ts. What regresses if + * this fails: a release binary built on a Bun that still trusts emSign ECC + * Root CA - G3 (or lacks the blocked-key check) passes the gate and ships. + * Synthetic bytes stand in for binaries; the gate reads bytes, not runtimes. + */ +import { describe, expect, test } from "bun:test"; +import { X509Certificate } from "node:crypto"; +import { BLOCKED_KEY_CHECK_MARKER, EMSIGN_ECC_G3_PEM, bunRuntimeTrustProblems } from "./check-bun-runtime-trust.ts"; + +const filler = Buffer.alloc(4096, 0x90); +const fixed = (version = "1.4.4") => Buffer.from(`\0Bun v${version} (38afee6c8)\0${BLOCKED_KEY_CHECK_MARKER}\0`, "latin1"); +const binary = (...parts: Buffer[]) => Buffer.concat([filler, ...parts, filler]); +const g3 = new X509Certificate(EMSIGN_ECC_G3_PEM); + +/** The PEM as Bun 1.3 embeds roots: 64-column lines. */ +function pem64(): Buffer { + const b64 = g3.raw.toString("base64").match(/.{1,64}/g)!.join("\n"); + return Buffer.from(`-----BEGIN CERTIFICATE-----\n${b64}\n-----END CERTIFICATE-----\n`, "latin1"); +} + +describe("Bun runtime trust gate", () => { + test("a 1.4.4 runtime with the blocked-key check and no G3 passes", () => { + expect(bunRuntimeTrustProblems(binary(fixed()))).toEqual([]); + expect(bunRuntimeTrustProblems(binary(fixed("1.5.0")))).toEqual([]); + }); + + test("an older Bun fails, and so does a binary with no Bun version at all", () => { + expect(bunRuntimeTrustProblems(binary(fixed("1.3.14")))).toEqual(["embedded Bun 1.3.14 is below 1.4.4"]); + expect(bunRuntimeTrustProblems(binary(fixed("1.4.3")))).toEqual(["embedded Bun 1.4.3 is below 1.4.4"]); + expect(bunRuntimeTrustProblems(binary(Buffer.from(BLOCKED_KEY_CHECK_MARKER)))).toEqual(["no embedded Bun version found"]); + }); + + test("emSign ECC Root CA - G3 fails the gate as PEM, as DER, or as its public key", () => { + expect(bunRuntimeTrustProblems(binary(fixed(), pem64()))).toContain("embeds emSign ECC Root CA - G3 as PEM"); + expect(bunRuntimeTrustProblems(binary(fixed(), Buffer.from(EMSIGN_ECC_G3_PEM)))).toContain("embeds emSign ECC Root CA - G3 as PEM"); + expect(bunRuntimeTrustProblems(binary(fixed(), g3.raw))).toContain("embeds emSign ECC Root CA - G3 as DER"); + const spki = g3.publicKey.export({ type: "spki", format: "der" }); + expect(bunRuntimeTrustProblems(binary(fixed(), spki))).toEqual(["embeds the emSign ECC Root CA - G3 public key"]); + }); + + test("another root's PEM is not mistaken for G3", () => { + const other = Buffer.from("-----BEGIN CERTIFICATE-----\nTUlJQ1RqQ0NBZE9nQXdJQkFnSUtQUFlIcVdod0R0cUxoREFL\n-----END CERTIFICATE-----\n"); + expect(bunRuntimeTrustProblems(binary(fixed(), other))).toEqual([]); + }); + + test("a runtime without the blocked-key check fails", () => { + expect(bunRuntimeTrustProblems(binary(Buffer.from("Bun v1.4.4 (38afee6c8)")))).toEqual([ + "lacks Bun's blocked-key check (oven-sh/bun#46300)", + ]); + }); +}); diff --git a/scripts/check-bun-runtime-trust.ts b/scripts/check-bun-runtime-trust.ts new file mode 100644 index 0000000000..524c2603f8 --- /dev/null +++ b/scripts/check-bun-runtime-trust.ts @@ -0,0 +1,114 @@ +/** + * Release gate: every compiled binary must carry a Bun runtime that does not + * trust the compromised eMudhra chain (Oct 2026). + * + * `bun build --compile` copies the Bun runtime, and with it Bun's root + * certificates and TLS checks, into every release binary. Bun 1.3.14 trusts + * "emSign ECC Root CA - G3", which the eight compromised eMudhra intermediate + * keys chain to. Bun 1.4.4 drops that root (NSS 3.131.1) and rejects any chain + * holding one of those keys (oven-sh/bun#46300). + * + * A binary fails the gate when: + * - its embedded Bun version (`Bun vX.Y.Z (rev)`) is missing or below 1.4.4; + * - it holds emSign ECC Root CA - G3 in any form: an embedded PEM block + * (Bun 1.3 embeds roots as PEM text), the certificate's DER (Bun 1.4 + * embeds DER), or its public key (SPKI DER); + * - it lacks the blocked-key check: `addBlockedSPKIForTesting`, the test + * hook #46300 added beside it. The eight key hashes themselves are not + * searchable: the compiler folds the constant array into code. + * + * Reads the files only; runs on any host (cross-compiled binaries never run). + * usage: bun scripts/check-bun-runtime-trust.ts ... + */ +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +export const MIN_BUN_VERSION = "1.4.4"; + +/** emSign ECC Root CA - G3. SHA-256 86:A1:EC:BA:08:9C:4A:8D:3B:BE:27:34:C6:12:BA:34:1D:81:3E:04:3C:F9:E8:A8:62:CD:5C:57:A3:6B:BE:6B */ +export const EMSIGN_ECC_G3_PEM = `-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +`; + +export const BLOCKED_KEY_CHECK_MARKER = "addBlockedSPKIForTesting"; + +const PEM_BEGIN = Buffer.from("-----BEGIN CERTIFICATE-----"); +const PEM_END = Buffer.from("-----END CERTIFICATE-----"); +const VERSION_RE = /Bun v(\d+)\.(\d+)\.(\d+) \([0-9a-f]+\)/g; + +const g3 = new X509Certificate(EMSIGN_ECC_G3_PEM); +const G3_DER = g3.raw; +const G3_SPKI = g3.publicKey.export({ type: "spki", format: "der" }); + +function compareVersions(a: readonly number[], b: readonly number[]): number { + for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i]! - b[i]!; + return 0; +} + +/** Decode a PEM body as Bun embeds it (it may sit in C string text: `\n`, quotes). */ +function pemBodyDer(body: Buffer): Buffer | null { + const cleaned = body.toString("latin1").replace(/\\n|["\s]/g, ""); + if (!/^[A-Za-z0-9+/]+=*$/.test(cleaned)) return null; + return Buffer.from(cleaned, "base64"); +} + +function holdsG3Pem(bytes: Buffer): boolean { + let at = bytes.indexOf(PEM_BEGIN); + while (at !== -1) { + const end = bytes.indexOf(PEM_END, at + PEM_BEGIN.length); + if (end === -1) return false; + const der = pemBodyDer(bytes.subarray(at + PEM_BEGIN.length, end)); + if (der !== null && der.equals(G3_DER)) return true; + at = bytes.indexOf(PEM_BEGIN, end + PEM_END.length); + } + return false; +} + +/** What is wrong with one binary's Bun runtime; empty when it passes. */ +export function bunRuntimeTrustProblems(bytes: Buffer): string[] { + const problems: string[] = []; + const text = bytes.toString("latin1"); + const versions = [...text.matchAll(VERSION_RE)].map((m) => [Number(m[1]), Number(m[2]), Number(m[3])]); + const min = MIN_BUN_VERSION.split(".").map(Number); + if (versions.length === 0) { + problems.push("no embedded Bun version found"); + } else { + const low = versions.filter((v) => compareVersions(v, min) < 0).map((v) => v.join(".")); + if (low.length > 0) problems.push(`embedded Bun ${[...new Set(low)].join(", ")} is below ${MIN_BUN_VERSION}`); + } + if (holdsG3Pem(bytes)) problems.push("embeds emSign ECC Root CA - G3 as PEM"); + if (bytes.includes(G3_DER)) problems.push("embeds emSign ECC Root CA - G3 as DER"); + if (bytes.includes(G3_SPKI)) problems.push("embeds the emSign ECC Root CA - G3 public key"); + if (!text.includes(BLOCKED_KEY_CHECK_MARKER)) problems.push("lacks Bun's blocked-key check (oven-sh/bun#46300)"); + return problems; +} + +if (import.meta.main) { + const files = process.argv.slice(2); + if (files.length === 0) { + console.error("usage: bun scripts/check-bun-runtime-trust.ts ..."); + process.exit(2); + } + let failed = false; + for (const file of files) { + const problems = bunRuntimeTrustProblems(readFileSync(file)); + if (problems.length === 0) { + console.log(`OK ${file}`); + } else { + failed = true; + for (const problem of problems) console.error(`FAIL ${file}: ${problem}`); + } + } + process.exit(failed ? 1 : 0); +} From 365cf4f05a44c3d2953a77f5e8be07df0911e0d5 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:31:10 -0400 Subject: [PATCH 07/21] ci(release): the older-CPU gate's control is an AVX2 instruction Since Bun 1.4, bun-linux-x64 and bun-linux-x64-baseline are one byte-identical runtime (1.4.4: sha256 10702245...ac235 in both release zips), so the default-target control build ran on qemu -cpu Nehalem and failed the "must SIGILL" control on this branch. The control is now a one-instruction AVX2 probe built on the runner: it runs natively (where the CPU has AVX2) and must die with SIGILL under the same qemu invocation, which is what makes "the baseline binary ran there" falsifiable. The control binary's build, upload and download go. Checked in an amd64 container: the probe exits 132 under -cpu Nehalem, as a Bun 1.3.14 default-target build does. --- .github/workflows/release.yml | 83 +++++++++++++---------------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c72a9597aa..d78481636d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,10 +116,12 @@ jobs: sha256sum plannotator-darwin-x64 > plannotator-darwin-x64.sha256 # Linux x64 (baseline: CPUID-dispatched SIMD, so it still runs on - # pre-AVX2 CPUs). Bun's default x64 target is x86-64-v3 and SIGILLs - # on Nehalem/Ivy Bridge class hardware. Proven by the emulated-CPU - # gate in `smoke-binaries`, which is itself proven by the - # default-target control binary compiled below. + # pre-AVX2 CPUs). Up to Bun 1.3 the default x64 target was + # x86-64-v3 and SIGILLed on Nehalem/Ivy Bridge class hardware; since + # Bun 1.4 bun-linux-x64 and bun-linux-x64-baseline are one + # byte-identical runtime, and the baseline target stays as the + # explicit ask. Proven by the emulated-CPU gate in `smoke-binaries`, + # which is itself proven by its AVX2 control. bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig --target=bun-linux-x64-baseline --define "__CLI_VERSION__=\"$VERSION\"" --outfile plannotator-linux-x64 sha256sum plannotator-linux-x64 > plannotator-linux-x64.sha256 @@ -160,20 +162,6 @@ jobs: bun build apps/paste-service/targets/bun.ts --compile --no-compile-autoload-bunfig --target=bun-windows-arm64 --outfile plannotator-paste-win32-arm64.exe sha256sum plannotator-paste-win32-arm64.exe > plannotator-paste-win32-arm64.exe.sha256 - # Control subject for the emulated-CPU gate in `smoke-binaries`. Same - # source, same Bun, same flags as the shipped Linux x64 binary — only the - # target differs (bun-linux-x64 is x86-64-v3/AVX2). The gate asserts this - # one dies with SIGILL on the emulated pre-AVX2 CPU, which is what makes - # "the baseline binary ran there" a falsifiable claim. Uploaded under its - # own artifact name so it can never reach the `binaries` artifact, the - # attestation subjects, or the GitHub Release (both of which resolve - # `binaries` by name). - - name: Compile baseline-gate control binary (default x64 target) - run: | - VERSION=$(jq -r .version package.json) - mkdir -p baseline-control - bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig --target=bun-linux-x64 --define "__CLI_VERSION__=\"$VERSION\"" --outfile baseline-control/control-cli-linux-x64-default - # Every binary carries the Bun runtime it was compiled with, root # certificates included. Fail unless each one embeds Bun >= 1.4.4, has # the blocked-key check, and holds no emSign ECC Root CA - G3 (the @@ -182,24 +170,16 @@ jobs: run: | set -euo pipefail binaries=() - for f in plannotator-* baseline-control/control-cli-linux-x64-default; do + for f in plannotator-*; do [[ "$f" == *.sha256 ]] || binaries+=("$f") done - # 6 CLI + 6 paste targets + the control: never a gate over nothing. - if (( ${#binaries[@]} < 13 )); then - echo "FAIL: expected at least 13 binaries, found ${#binaries[@]}" >&2 + # 6 CLI + 6 paste targets: never a gate over nothing. + if (( ${#binaries[@]} < 12 )); then + echo "FAIL: expected at least 12 binaries, found ${#binaries[@]}" >&2 exit 1 fi bun scripts/check-bun-runtime-trust.ts "${binaries[@]}" - - name: Upload baseline-gate control binary - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: baseline-gate-control - path: baseline-control/control-cli-linux-x64-default - if-no-files-found: error - retention-days: 1 - - name: Upload artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -261,45 +241,42 @@ jobs: echo "FAIL: could not install qemu-user after 3 attempts" >&2 exit 1 - - name: Download baseline-gate control binary - if: runner.os == 'Linux' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: baseline-gate-control - path: control - # Control for the gate below. Without it, "the baseline binary ran under # -cpu Nehalem" is unfalsifiable: a qemu that quietly emulated AVX2, an - # ignored -cpu flag, or a gate that never really executed the artifact - # would all look exactly like a pass. This runs the SAME source built with - # Bun's default x64 target (x86-64-v3/AVX2) through the SAME qemu - # invocation and requires it to die, so a green gate means the gate can - # actually tell the two builds apart. - - name: Control - default-target x64 binary must SIGILL on the emulated CPU + # ignored -cpu flag, or a gate that never really executed anything would + # all look exactly like a pass. Up to Bun 1.3 the control was the same + # source built for Bun's default x64 target (x86-64-v3); since Bun 1.4 + # bun-linux-x64 and bun-linux-x64-baseline are one byte-identical + # runtime, so that build runs on Nehalem too. The control is now one + # AVX2 instruction: it runs on the runner and must die with SIGILL under + # the SAME qemu invocation, so a green gate means qemu enforced the CPU. + - name: Control - an AVX2 instruction must SIGILL on the emulated CPU if: runner.os == 'Linux' timeout-minutes: 5 env: - CONTROL_BINARY: control/control-cli-linux-x64-default CPU_MODEL: Nehalem RUN_TIMEOUT: 120s run: | set -euo pipefail ulimit -c 0 - chmod +x "$CONTROL_BINARY" + probe_dir="$(mktemp -d)" + printf '%s\n' 'int main(void) { __asm__ volatile("vpaddd %%ymm0, %%ymm0, %%ymm0" ::: "xmm0"); return 0; }' > "$probe_dir/avx2.c" + gcc -O0 -o "$probe_dir/avx2-probe" "$probe_dir/avx2.c" + # On a runner whose CPU has AVX2 the probe itself must run. + if grep -qw avx2 /proc/cpuinfo; then + "$probe_dir/avx2-probe" + fi status=0 - timeout --kill-after=5s "$RUN_TIMEOUT" qemu-x86_64 -cpu "$CPU_MODEL" "$CONTROL_BINARY" --version \ - > control.log 2>&1 || status=$? - cat control.log || true - + timeout --kill-after=5s "$RUN_TIMEOUT" qemu-x86_64 -cpu "$CPU_MODEL" "$probe_dir/avx2-probe" || status=$? # 132 == 128 + SIGILL(4); qemu-user re-raises the guest signal and # timeout propagates signal deaths the same way. if [ "$status" -ne 132 ]; then - echo "FAIL: control binary $CONTROL_BINARY (built with bun-linux-x64, AVX2) exited ${status} under qemu-x86_64 -cpu ${CPU_MODEL}; expected 132 (SIGILL)." >&2 - echo "The older-CPU gate cannot distinguish a baseline build from a non-baseline one on this runner, so its result would be meaningless. Fix the harness before trusting the gate." >&2 + echo "FAIL: an AVX2 instruction exited ${status} under qemu-x86_64 -cpu ${CPU_MODEL}; expected 132 (SIGILL)." >&2 + echo "The older-CPU gate cannot show that qemu enforces the CPU model on this runner, so its result would be meaningless." >&2 exit 1 fi - echo "OK: default-target control binary died with SIGILL on ${CPU_MODEL}; the gate below can fail." + echo "OK: an AVX2 instruction died with SIGILL on ${CPU_MODEL}; the gate below can fail." - name: Smoke-test Linux x64 baseline CPU compatibility if: runner.os == 'Linux' @@ -328,7 +305,7 @@ jobs: # Nehalem has SSE4.2 but no AVX, AVX2 or BMI instructions. Execute the # downloaded release artifacts, not a separately compiled probe. The - # control step above proves a non-baseline build fails right here. + # control step above proves an AVX2 instruction fails right here. timeout --kill-after=5s "$RUN_TIMEOUT" qemu-x86_64 -cpu "$CPU_MODEL" "$BINARY" --help \ || fail "CLI --help did not run on the emulated CPU" "$BINARY" From 16d8ddd30c8909f7b9f77f69106f66bb2689f420 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:31:10 -0400 Subject: [PATCH 08/21] chore: raise host runtime floors for the eMudhra fix - OpenCode plugin engines.bun >=1.4.4 (first Bun that drops emSign ECC Root CA - G3 and blocks the compromised keys). - Pi extension engines.node >=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1: the Oct 20-21 Node security releases (NSS 3.131.1 roots, tls: reject chains containing blocked public keys, nodejs/node#66871). - @plannotator/server drops its `bun >=1.0.0` peer. It is private, and the peer only resolved bun@1.4.0 and its @oven/bun-* packages into bun.lock; those 13 entries go (hand-edited: a re-save also re-nests unrelated @effect entries). `bun install --frozen-lockfile` passes. Bun and npm only warn on engines: a warning and documentation, never a blocked install. --- apps/opencode-plugin/package.json | 2 +- apps/pi-extension/package.json | 3 +++ bun.lock | 29 ----------------------------- packages/server/package.json | 3 --- 4 files changed, 4 insertions(+), 33 deletions(-) diff --git a/apps/opencode-plugin/package.json b/apps/opencode-plugin/package.json index 8126ce75cb..fa4637e04f 100644 --- a/apps/opencode-plugin/package.json +++ b/apps/opencode-plugin/package.json @@ -54,6 +54,6 @@ "@plannotator/shared": "workspace:*" }, "engines": { - "bun": ">=1.0.0" + "bun": ">=1.4.4" } } diff --git a/apps/pi-extension/package.json b/apps/pi-extension/package.json index f791b2938d..ba2ee81ef1 100644 --- a/apps/pi-extension/package.json +++ b/apps/pi-extension/package.json @@ -75,6 +75,9 @@ "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.79.1" }, + "engines": { + "node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1" + }, "devDependencies": { "glimpseui": "^0.8.0", "@earendil-works/pi-coding-agent": ">=0.79.1", diff --git a/bun.lock b/bun.lock index cff8c01131..e78b5885c4 100644 --- a/bun.lock +++ b/bun.lock @@ -357,9 +357,6 @@ "@modelcontextprotocol/client": "2.3.1", "glimpseui": "^0.8.0", }, - "peerDependencies": { - "bun": ">=1.0.0", - }, }, "packages/shared": { "name": "@plannotator/shared", @@ -1070,30 +1067,6 @@ "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GCpf8QuFLsyioVawP5HrMxA1ZRBlu6Hq9RNnSc3UTUWAzIxBso9trjoZczw1HdgpqSssFkszfIV2zmOzFTjhkw=="], - - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cIrhwOr0SPEraewznhC+c/k6TG8bwFn5uZ4EJuXwjiKJLcAF36q7/bGjWkeXSe48JwMcPRUR054JXF7+cRwSSA=="], - - "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.4.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-09x7wnjMR6M5KGBDBhVl2CpfoCIQOkVDbPX2KfIhpXv4N6grbWE7dfLPw/Ydi9gaUMGhU7UKhoz444Nu6RCycA=="], - - "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.4.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dRwzti/qJqV1HWplU27iUWUqp+f2DtFSf2yqQKSb+HH2dDOC//Uqd9u/A5h1DMsLszfP5OGP9UwQIKxVwFODaA=="], - - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y5yAtCbHK6JjprXEtkdklDQFPADgs+CkfcliyY5g4JJ8baGHyQSrfpSkX3XVJ2C+aBLsdwNDdW+oczMsAwx6uA=="], - - "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.4.0", "", { "os": "android", "cpu": "arm64" }, "sha512-HpPIxJfDNPBPhiBNMyZoo/dOLijARfsx5j72vNuLtaTvl0Hh7HUculxjsOQ2WSyGoCgqXMEr1Qqjab1im9u1RA=="], - - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RUjAAkJ/CdNV++zVxyANWshPc73CECYsfhk0fWAkoJjtywxJ2BwXzI6nopBBDMfs0HS+fhRGn6zGwU8ccxLeJg=="], - - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Du44zebtPXJujvMLmtIxEQ6ykOhYt7L/Q+YIGVm+Yy+Pj/fpOnq60ggwIpKp/pGAFbYHNiTrA3JTjuZ9MTbZIg=="], - - "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.4.0", "", { "os": "android", "cpu": "x64" }, "sha512-u++KyLlfMn36yWz+AgJs+fZtS46UFDNpSSZhrcitkytONtNwq0X6Q9BDVEFXxYl/+Eec0xme1rb6MgW+U35WeA=="], - - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-C1Dv+ISL8YKEKM9jAHzNifOcRUoziy6UMxh+yVXjUCP6QnbRhENDHLaIWWkQZJyBLTn0I3xozflorAlHiGzGqA=="], - - "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FBAYaQpJBP0asgqzL6NFUfjdQqsV+kvTpJ/eWxPKj+RcDgIfPSuE8kvQuPYu5pa8u8JTujYMjmuyvHxVuQsInA=="], - - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jRKv1NPLznMSZY5BEWciMF7zv0Tiyo2pQSxAJ3w+YWJ6y3VWNJQQQdLlV5Jx8lbOFDrJdrc9dD3GV17k3BP41A=="], - "@oxc-project/types": ["@oxc-project/types@0.154.0", "", {}, "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA=="], "@pierre/diffs": ["@pierre/diffs@1.5.2", "", { "dependencies": { "@pierre/theme": "2.0.0", "@pierre/theming": "1.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "9.0.0", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-QVxIWQEnNoko22k63maERzozd29+FtaUNpRrZiw603WMe5R1mFSED+jMtPkcVP3SDYS1DWg5K7sa7sJbr3xkNQ=="], @@ -1644,8 +1617,6 @@ "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], - "bun": ["bun@1.4.0", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.0", "@oven/bun-darwin-x64": "1.4.0", "@oven/bun-freebsd-aarch64": "1.4.0", "@oven/bun-freebsd-x64": "1.4.0", "@oven/bun-linux-aarch64": "1.4.0", "@oven/bun-linux-aarch64-android": "1.4.0", "@oven/bun-linux-aarch64-musl": "1.4.0", "@oven/bun-linux-x64": "1.4.0", "@oven/bun-linux-x64-android": "1.4.0", "@oven/bun-linux-x64-musl": "1.4.0", "@oven/bun-windows-aarch64": "1.4.0", "@oven/bun-windows-x64": "1.4.0" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-iRiFkc2W7UVpCyZXO9tod45TP9QCyN19fWqbpeN/jaM/K7uzeHYx/OSPsahMJazGKBgPsnxRt+4Jc43d8BcHZw=="], - "bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], diff --git a/packages/server/package.json b/packages/server/package.json index eb526b2f28..cc5682a7a1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -56,9 +56,6 @@ "uqr": "^0.1.3", "zod": "4.6.7" }, - "peerDependencies": { - "bun": ">=1.0.0" - }, "devDependencies": { "@modelcontextprotocol/client": "2.3.1", "glimpseui": "^0.8.0" From 1eb366cc53d331bfe74cbffbbee8fe8b21aac3e1 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:32:25 -0400 Subject: [PATCH 09/21] chore: no compile path builds on a Bun older than 1.4.4 - scripts/require-bun.ts fails a compile on Bun < MIN_BUN_VERSION; the t3-code build and tests/manual/local/sandbox-codex.sh run it first. - tests/manual/ssh/Dockerfile installs bun-v1.4.4, not whatever is latest. - The build guide asks for bun >= 1.4.4 (was ">= 1.3.14") and says why; it documents the release trust gate. - The Pi and OpenCode READMEs state the host runtime floors. --- apps/opencode-plugin/README.md | 2 ++ apps/pi-extension/README.md | 5 +++++ docs/agent-guides/project-and-development.md | 8 +++++--- packages/t3-code/package.json | 2 +- scripts/require-bun.ts | 13 +++++++++++++ tests/manual/local/sandbox-codex.sh | 1 + tests/manual/ssh/Dockerfile | 4 ++-- 7 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 scripts/require-bun.ts diff --git a/apps/opencode-plugin/README.md b/apps/opencode-plugin/README.md index 7e132ea746..60f76b42d7 100644 --- a/apps/opencode-plugin/README.md +++ b/apps/opencode-plugin/README.md @@ -64,6 +64,8 @@ Add to your `opencode.json`: Restart OpenCode. By default, the `submit_plan` tool is available to OpenCode's `plan` agent, not to `build` or other primary agents. +In Bun-hosted OpenCode the embedded server runs on OpenCode's own Bun. Use an OpenCode build on **Bun 1.4.4 or later**: older Bun still trusts the compromised eMudhra certificate chain (October 2026). The package's `engines` field states this floor; it warns, it does not block. + > **OpenCode 1 slash commands:** Run the install script to get `/plannotator-review`, `/plannotator-annotate`, and `/plannotator-last`: > ```bash > curl -fsSL https://plannotator.ai/install.sh | bash diff --git a/apps/pi-extension/README.md b/apps/pi-extension/README.md index 472f533c1e..6d42599a17 100644 --- a/apps/pi-extension/README.md +++ b/apps/pi-extension/README.md @@ -39,6 +39,11 @@ Plannotator requires **Pi 0.79.1 or newer**. Updating only the Plannotator extension does not repair the security behavior of an older Pi host; update Pi itself before loading the extension. +Run Pi on **Node 22.23.4+, 24.22.1+ or 26.12.1+**. These October 2026 security +releases stop trusting the compromised eMudhra certificate chain. On an older +Node, HTTPS calls made inside Pi still trust it. The package's `engines` field +states this floor; npm warns but does not block the install. + Pi 0.79 introduced project trust for repository-local inputs. In interactive sessions, Pi asks before loading project settings, instructions, resources, and packages, and can save the decision for that working directory. Plannotator diff --git a/docs/agent-guides/project-and-development.md b/docs/agent-guides/project-and-development.md index 0b86a7a290..c9e3feb0a2 100644 --- a/docs/agent-guides/project-and-development.md +++ b/docs/agent-guides/project-and-development.md @@ -152,7 +152,7 @@ claude --plugin-dir ./apps/hook ## Requirements -- Bun runtime +- Bun runtime (>= 1.4.4 to compile binaries; `scripts/require-bun.ts` checks it) - Claude Code with plugin/hooks support, or OpenCode - Cross-platform: macOS (`open`), Linux (`xdg-open`), Windows (`start`) @@ -209,12 +209,14 @@ bun run build:hook && bun run build:opencode # For OpenCode plugin Running only `build:hook` after review-editor changes will copy stale HTML files. When testing locally with a compiled binary, the full sequence is: ```bash -bun run --cwd apps/review build && bun run build:hook && \ +bun scripts/require-bun.ts && bun run --cwd apps/review build && bun run build:hook && \ bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig \ --define "__CLI_VERSION__=\"0.0.0-dev\"" --outfile ~/.local/bin/plannotator ``` -These are the flags `.github/workflows/release.yml` compiles with; without the `__CLI_VERSION__` define `plannotator --version` falls back to `plannotator dev` and the binary cannot be told apart from any other local build. On macOS build with bun >= 1.3.14 (CI pins 1.4.4): 1.3.12 loses the linker signature and the compiled binary is SIGKILLed on launch with no error (#541). +These are the flags `.github/workflows/release.yml` compiles with; without the `__CLI_VERSION__` define `plannotator --version` falls back to `plannotator dev` and the binary cannot be told apart from any other local build. Build with bun >= 1.4.4 (what CI pins). The binary carries the compiling Bun's runtime, root certificates included: Bun before 1.4.4 still trusts emSign ECC Root CA - G3 and lacks the blocked-key check for the compromised eMudhra keys (Oct 2026). On macOS, 1.3.12 also loses the linker signature, and the compiled binary is SIGKILLed on launch with no error (#541). + +**Release trust gate.** `release.yml` runs `scripts/check-bun-runtime-trust.ts` over every compiled binary before upload. A binary fails when its embedded Bun is below 1.4.4, when it holds emSign ECC Root CA - G3 (as PEM, DER or public key), or when it lacks the blocked-key check (oven-sh/bun#46300; marker `addBlockedSPKIForTesting`). The script reads bytes only, so the Linux builder also checks the darwin and Windows binaries. Raise `MIN_BUN_VERSION` there together with the workflow pins. **Patched dependency.** `patches/@opencode-ai%2Fsdk@1.19.3.patch` (root `patchedDependencies`) makes the SDK's SSE abort handler catch `reader.cancel()`. On Bun 1.4 that promise rejects once the aborted fetch has errored the body, and the unhandled rejection ends the process: every OpenCode Ask AI answer or Stop aborts the event stream. `packages/ai/providers/opencode-sdk.test.ts` ("through the real client") fails without it. A new SDK version needs the patch carried over (`bun patch @opencode-ai/sdk`) until upstream catches the promise. diff --git a/packages/t3-code/package.json b/packages/t3-code/package.json index ec42c07152..af4d3da8a6 100644 --- a/packages/t3-code/package.json +++ b/packages/t3-code/package.json @@ -9,7 +9,7 @@ "./usage": "./usage.ts" }, "scripts": { - "build": "bun build ../../apps/hook/server/index.ts --target bun --compile --outfile dist/plannotator", + "build": "bun ../../scripts/require-bun.ts && bun build ../../apps/hook/server/index.ts --target bun --compile --outfile dist/plannotator", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/scripts/require-bun.ts b/scripts/require-bun.ts new file mode 100644 index 0000000000..3b9dd8e8a5 --- /dev/null +++ b/scripts/require-bun.ts @@ -0,0 +1,13 @@ +/** + * Exit 1 unless the running Bun is at least the release floor. Run it before + * a `bun build --compile`: the binary carries the running Bun's runtime, root + * certificates and TLS checks included, so an older Bun ships a binary that + * still trusts the compromised eMudhra chain (see check-bun-runtime-trust.ts). + * usage: bun scripts/require-bun.ts + */ +import { MIN_BUN_VERSION } from "./check-bun-runtime-trust.ts"; + +if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) { + console.error(`Bun ${Bun.version} is too old to compile Plannotator: use Bun >= ${MIN_BUN_VERSION} (bun upgrade).`); + process.exit(1); +} diff --git a/tests/manual/local/sandbox-codex.sh b/tests/manual/local/sandbox-codex.sh index 08c482d944..a818674f09 100755 --- a/tests/manual/local/sandbox-codex.sh +++ b/tests/manual/local/sandbox-codex.sh @@ -48,6 +48,7 @@ echo "" # Build the plannotator binary echo "Compiling plannotator binary..." cd "$PROJECT_ROOT" +bun scripts/require-bun.ts || exit 1 bun run build:hook > /dev/null 2>&1 bun run build:review > /dev/null 2>&1 bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig --outfile ~/.local/bin/plannotator 2>&1 diff --git a/tests/manual/ssh/Dockerfile b/tests/manual/ssh/Dockerfile index b95f7f4d42..4477373117 100644 --- a/tests/manual/ssh/Dockerfile +++ b/tests/manual/ssh/Dockerfile @@ -8,8 +8,8 @@ RUN apt-get update && apt-get install -y \ openssh-server \ && rm -rf /var/lib/apt/lists/* -# Install Bun -RUN curl -fsSL https://bun.sh/install | bash +# Install Bun, pinned to the release floor (scripts/check-bun-runtime-trust.ts) +RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.4.4" ENV PATH="/root/.bun/bin:$PATH" # Set up SSH From 1700c15104a44e355b8680e260454067705e7d51 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:33:56 -0400 Subject: [PATCH 10/21] chore(renovate): keep the Bun pin, but raise a PR for each Bun patch Minor and major bun-version bumps stay off (signed macOS builds need checking first, v0.17.9). Patch releases, where Bun ships security fixes, now raise their own "Bun runtime" PR after the 7-day release age, never automerged, with a note: smoke-binaries (macOS) and the Bun runtime trust gate must pass, and dast.yml's BUN_VERSION and image digest are bumped by hand. 1.4.4's eMudhra fix would not have been missed. --- .github/renovate.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 8799cf9f42..8412955f93 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -22,10 +22,23 @@ "enabled": true }, { - "description": "Do not bump the bun-version input passed to oven-sh/setup-bun. See v0.17.9 hotfix.", + "description": "Do not bump the bun-version input passed to oven-sh/setup-bun to a new minor or major: verify signed macOS builds first. See v0.17.9 hotfix.", "matchManagers": ["github-actions"], "matchDepNames": ["bun"], + "matchUpdateTypes": ["minor", "major"], "enabled": false + }, + { + "description": "Raise a PR for every Bun patch release: Bun ships security fixes in patches (1.4.4 dropped the compromised eMudhra root and blocked its keys), so one is never missed. Its own PR, never automerged, after the 7-day release age.", + "matchManagers": ["github-actions"], + "matchDepNames": ["bun"], + "matchUpdateTypes": ["patch"], + "enabled": true, + "automerge": false, + "groupName": "Bun runtime", + "prBodyNotes": [ + "Bun patch release. Before merging: release.yml's smoke-binaries (macOS leg: the signed binary must launch) and the Bun runtime trust gate must pass, and bump BUN_VERSION and TARGET_IMAGE (digest from the registry) in dast.yml by hand." + ] } ] } From d8bf9f7a9e2d13dde9911e8352da3feb8d9b984e Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:48:13 -0400 Subject: [PATCH 11/21] chore(guides-show): re-pin the viewer manifest after the DiagramViewer fix The viewer bundles packages/ui's DiagramViewer, so the composer fix changed viewer.js's hash. Regenerated with build:viewer + sync:manifest on Bun 1.4.4; the CI build produced the identical manifest. Without the DiagramViewer change, Bun 1.4.4 reproduces main's manifest byte for byte: the bump itself does not move it. --- packages/core/guide-viewer-manifest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/guide-viewer-manifest.ts b/packages/core/guide-viewer-manifest.ts index 11ca0223cb..1cd4a8362d 100644 --- a/packages/core/guide-viewer-manifest.ts +++ b/packages/core/guide-viewer-manifest.ts @@ -5,9 +5,9 @@ import type { GuideViewerAssets } from "./guide-format"; export const GUIDE_VIEWER_MANIFEST: Omit = { - js: "viewer.QlwmHYmZ.js", + js: "viewer.8xW_0_vs.js", css: "viewer.DlVTzEs5.css", - jsIntegrity: "sha384-iwef3lANt/GB0/RukuLQccOrqArj8WJJAfsOHbTcSwMOYRgPVjZiTQ6O6ce+sMny", + jsIntegrity: "sha384-0JMSXUjvmndKFRKOXcAXYjHpvAjfLq/XVeOXASyaaEBS9TGAFYMxy3Ia307r6iNF", cssIntegrity: "sha384-6vGDSDkUF7SiDQirhrnrtCw95smxSFsz6NGx6fYCnEf4bI9I2sbYB/berVRuor/x", langs: { "astro": "chunks/astro.Ts5EKq2l.js", From 0430ea05ad77aa5bf4159069e16c058c8c4cb709 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 26 Oct 2026 15:48:13 -0400 Subject: [PATCH 12/21] fix(inbox): a paused player keeps its streamed answer on Bun 1.4 Bun 1.4 pulls a streamed body only as fast as the client reads (1.3.14 read the whole range ahead into memory). So a player paused with a full buffer, or a slow download, now leaves a live answer unpulled, and the 30 s idle watchdog cut it short; measured with a 2 s watchdog: a 200 KB/s client lost its 8 MiB answer at 14 s on 1.4.4, never on 1.3.14. Bun 1.4.4 also fixed what the watchdog was for: it cancels a client gone before the answer starts, and FileHandle.close() during a read closes the descriptor. The watchdog stays as a backstop at 5 minutes. The comments and the Inbox guide say what each Bun version does. The 8 MiB cap stays: it bounds one answer on any runtime. --- docs/agent-guides/inbox-files-and-guides.md | 2 +- packages/server/inbox-byte-range.ts | 31 +++++++++++++-------- packages/server/inbox-video.test.ts | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/agent-guides/inbox-files-and-guides.md b/docs/agent-guides/inbox-files-and-guides.md index 1568faf792..87ca6058dc 100644 --- a/docs/agent-guides/inbox-files-and-guides.md +++ b/docs/agent-guides/inbox-files-and-guides.md @@ -14,7 +14,7 @@ - `GET /api/inbox/attachments/:id/image[?version=sent]`: kind `image` only (`415 attachment_not_image` otherwise), `Content-Type` re-sniffed from the bytes served (never the record's or the name's), CSP `sandbox; default-src 'none'; style-src 'unsafe-inline'` (the message-images policy), `inline`, `ETag` = sha256 (`304`), `Cache-Control: private, no-cache`, `X-Image-Width/Height`. The window draws images only by ``, so an SVG never runs. - `GET /api/inbox/attachments/:id/video[?version=sent]` (design record "inbox-video", approved 2026-10-15; not in this repo): kind `video` only (`415 attachment_not_video` otherwise), GET only (`405`), `Content-Type` re-sniffed from the bytes served (`video/mp4`, `video/quicktime`, `video/webm`), `inline`, the message-images CSP, `Cache-Control: private, no-cache`, and HTTP Range (below). The window plays it in `