diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c74d39..d956641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.4.1] - 2026-11-09 + +### Added + +- Compatibility manifest entry for ChatGPT.app 26.1103.60933, verified end to end against Helium +- `run` explains unavailable-browser errors without treating them as proof of an incompatible ChatGPT.app release or replaying failed tool calls. + ## [0.4.0] - 2026-11-04 ### Added @@ -91,7 +98,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Disabled-by-default Claude Code plugin with setup, doctor, and browser skills - stderr secret redaction and strict stdout protocol purity -[Unreleased]: https://github.com/stickerdaniel/browserjack/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/stickerdaniel/browserjack/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/stickerdaniel/browserjack/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/stickerdaniel/browserjack/compare/v0.3.3...v0.4.0 [0.3.3]: https://github.com/stickerdaniel/browserjack/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/stickerdaniel/browserjack/compare/v0.3.1...v0.3.2 diff --git a/compatibility/manifest.json b/compatibility/manifest.json index da2a464..c5e9d8a 100644 --- a/compatibility/manifest.json +++ b/compatibility/manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "generatedAt": "2026-11-04", + "generatedAt": "2026-11-09", "entries": [ { "bundleId": "com.openai.codex", @@ -33,6 +33,14 @@ "teamId": "2DC432GLL2", "architectures": ["arm64"], "browserClientSha256": "6704e0f3b0c94d3e041a22488550d4c29e6252a934163d0aa539a7d947d5d17b" + }, + { + "bundleId": "com.openai.codex", + "appVersion": "26.1103.60933", + "pluginVersion": "26.1103.60933", + "teamId": "2DC432GLL2", + "architectures": ["arm64"], + "browserClientSha256": "becbcc75697f519cad57e002267f14581dd759d235021411ed61637b961ce469" } ] } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 88b48c4..7851248 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -28,6 +28,8 @@ The installed shim pins the exact Node.js executable used at install time. A Hom `doctor --live` verifies that the runtime starts and that `agent.browsers.list()` works; zero connected backends is not a runtime failure. Make sure the ChatGPT/Codex extension is installed and enabled in the browser profile you intend to use, and that ChatGPT.app is running. +`Browser is not available: chrome` means the runtime could not find the requested browser. `No browser is available` means discovery found no usable backend. A disconnected extension or an unavailable browser ID can cause these errors; they do not by themselves establish that ChatGPT.app is incompatible. `run` appends this distinction to failed tool results. It does not replay tool calls, which may already have performed actions before failing. + ## The browser asks for site or automation permission OpenAI's extension has its own permission layer, independent of Claude Code's permission mode. When a site prompts, choose `Allow for this site` to persist an origin-specific entry. Avoid `Allow for all sites` or any global never-ask setting; Browserjack does not enable those and you should not either. diff --git a/package-lock.json b/package-lock.json index 0419d81..2bfdcfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "cpu": [ "arm64" ], diff --git a/package.json b/package.json index 92d7e6b..3c40e66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "description": "A local macOS MCP bridge that lets Claude Code and other MCP clients reuse OpenAI's installed Codex browser runtime.", "keywords": [ "browser-automation", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 6a6a5fc..e7b60ea 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "browserjack", "displayName": "Browserjack", - "version": "0.4.0", + "version": "0.4.1", "description": "Use OpenAI's locally installed Codex browser runtime from Claude Code (macOS)", "author": { "name": "Daniel Sticker" diff --git a/plugin/skills/setup/SKILL.md b/plugin/skills/setup/SKILL.md index 84206d7..f7c4239 100644 --- a/plugin/skills/setup/SKILL.md +++ b/plugin/skills/setup/SKILL.md @@ -8,7 +8,7 @@ Explain the trust boundary before installation: the bridge can control authentic For a published release, install an exact reviewed version: ```bash -npx --yes browserjack@0.4.0 setup --client plugin --scope user +npx --yes browserjack@0.4.1 setup --client plugin --scope user ``` For a source checkout, run: diff --git a/src/runtime/server.ts b/src/runtime/server.ts index f220273..1f610b5 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -12,7 +12,7 @@ import { createRuntimeLaunch } from "./launch.js"; import { readLines } from "./lines.js"; import { createTurnState, injectTurnMetadata } from "./metadata.js"; import { RedactingTransform } from "./redact.js"; -import { annotateToolError } from "./tool-errors.js"; +import { annotateToolError, browserUnavailableNotice } from "./tool-errors.js"; const MAX_MCP_LINE_BYTES = 16 * 1024 * 1024; @@ -70,10 +70,14 @@ async function forwardChildOutput( browserClientUrl: string, outdatedAppNotice: () => Promise, ): Promise { + const notices = async (errorText: string): Promise => + [browserUnavailableNotice(errorText), await outdatedAppNotice()].filter( + (notice) => notice !== null, + ); for await (const line of readLines(output, MAX_MCP_LINE_BYTES)) { await writeWithBackpressure( process.stdout, - (await annotateToolError(line, outdatedAppNotice)) ?? + (await annotateToolError(line, notices)) ?? addBrowserRuntimeInstructions(line, browserClientUrl), ); } diff --git a/src/runtime/tool-errors.ts b/src/runtime/tool-errors.ts index fb6fd41..f623552 100644 --- a/src/runtime/tool-errors.ts +++ b/src/runtime/tool-errors.ts @@ -1,11 +1,23 @@ import { isJsonObject } from "../lib/json.js"; -// Appends a Browserjack notice to a failed tools/call result, so the agent sees -// the likely cause next to OpenAI's error text. Resolves to null for every other -// line and when there is no notice, and only then looks the notice up. +// OpenAI reports an unavailable browser when discovery cannot match its ID or +// family. The error alone says nothing about app/runtime compatibility. +const NO_AVAILABLE_BROWSER = + /^(?:Error: )?(?:Browser is not available: [^\r\n]+|No browser is available)$/m; + +export function browserUnavailableNotice(errorText: string): string | null { + return NO_AVAILABLE_BROWSER.test(errorText) + ? "The runtime could not find the requested browser. A disconnected extension or an unavailable browser ID can cause this error. This error alone does not establish that ChatGPT.app is incompatible." + : null; +} + +// Appends Browserjack notices to a failed tools/call result, so the agent sees +// the likely cause next to OpenAI's error text. `notices` receives the result's +// joined text and runs only for failed results. Resolves to null for every +// other line and when there are no notices. export async function annotateToolError( line: string, - notice: () => Promise, + notices: (errorText: string) => Promise, ): Promise { let message: unknown; try { @@ -21,10 +33,18 @@ export async function annotateToolError( ) { return null; } - const text = await notice(); - if (text === null) { + const errorText = message.result.content + .map((item) => + isJsonObject(item) && item.type === "text" && typeof item.text === "string" ? item.text : "", + ) + .join("\n"); + const texts = await notices(errorText); + if (texts.length === 0) { return null; } - const content = [...message.result.content, { type: "text", text: `browserjack: ${text}` }]; + const content = [ + ...message.result.content, + ...texts.map((text) => ({ type: "text", text: `browserjack: ${text}` })), + ]; return `${JSON.stringify({ ...message, result: { ...message.result, content } })}\n`; } diff --git a/test/latest.test.mjs b/test/latest.test.mjs index ccf4141..c5bc530 100644 --- a/test/latest.test.mjs +++ b/test/latest.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { createOutdatedAppCheck, isOlderVersion, parseNewestBuild } from "../dist/compat/latest.js"; -import { annotateToolError } from "../dist/runtime/tool-errors.js"; +import { annotateToolError, browserUnavailableNotice } from "../dist/runtime/tool-errors.js"; // Trimmed from OpenAI's appcast on 2026-11-04; the first item is cut down to // its delta enclosure to exercise the full-zip requirement. @@ -21,15 +21,16 @@ const appcast = ` `; +function failedResult(text) { + return JSON.stringify({ + jsonrpc: "2.0", + id: 3, + result: { content: [{ type: "text", text }], isError: true }, + }); +} + // The js tool result a goto produced on ChatGPT.app 26.1021 with extension 1.26.1102. -const failedToolLine = JSON.stringify({ - jsonrpc: "2.0", - id: 3, - result: { - content: [{ type: "text", text: "Cannot read properties of undefined (reading 'timeoutMs')" }], - isError: true, - }, -}); +const failedToolLine = failedResult("Cannot read properties of undefined (reading 'timeoutMs')"); test("takes the newest feed item that has a full arm64 zip", () => { assert.deepEqual(parseNewestBuild(appcast), { @@ -87,7 +88,7 @@ test("reads the feed once per day and retries a failure after ten minutes", asyn }); test("appends the notice to a failed tool result", async () => { - const line = await annotateToolError(failedToolLine, async () => "Update ChatGPT.app."); + const line = await annotateToolError(failedToolLine, async () => ["Update ChatGPT.app."]); const { result } = JSON.parse(line ?? "null"); assert.equal(result.isError, true); assert.deepEqual(result.content.at(-1), { @@ -107,5 +108,67 @@ test("leaves successful results alone without looking up a notice", async () => }); assert.equal(await annotateToolError(success, unexpectedNotice), null); assert.equal(await annotateToolError("not json", unexpectedNotice), null); - assert.equal(await annotateToolError(failedToolLine, async () => null), null); + assert.equal(await annotateToolError(failedToolLine, async () => []), null); +}); + +test("annotates unavailable browsers without assuming an extension connection", async () => { + // The first result was captured from ChatGPT.app 26.1103 with an unavailable Edge backend. + for (const error of [ + "Browser is not available: edge", + "Browser is not available: chrome", + "No browser is available", + "Browser is not available: dot", + "Browser is not available: 42", + ]) { + const line = await annotateToolError(failedResult(error), async (errorText) => + [browserUnavailableNotice(errorText)].filter((notice) => notice !== null), + ); + const { result } = JSON.parse(line ?? "null"); + assert.equal(result.isError, true); + assert.deepEqual(result.content[0], { type: "text", text: error }); + assert.match( + result.content.at(-1).text, + /^browserjack: The runtime could not find the requested browser/, + ); + } + assert.equal(browserUnavailableNotice("Cannot read properties of undefined"), null); + assert.equal(browserUnavailableNotice("A page says Browser is not available: chrome"), null); +}); + +test("reads only text content and preserves mixed content and metadata", async () => { + const message = { + jsonrpc: "2.0", + id: 3, + result: { + isError: true, + _meta: { "codex/telemetry": { error_code: "browser_error" } }, + content: [ + { type: "text", text: "Browser is not available: edge" }, + { type: "text", text: 'Reported browsers: [{"family":"chrome","id":"1"}]' }, + { type: "image", mimeType: "image/png", data: "AA==" }, + { type: "resource", resource: { uri: "test:log", text: "resource text" } }, + null, + ], + }, + }; + const notices = ["Diagnostic", "Update notice"]; + const output = JSON.parse( + await annotateToolError(JSON.stringify(message), async (text) => { + assert.equal( + text, + 'Browser is not available: edge\nReported browsers: [{"family":"chrome","id":"1"}]\n\n\n', + ); + return notices; + }), + ); + assert.deepEqual(output, { + ...message, + result: { + ...message.result, + content: [ + ...message.result.content, + ...notices.map((text) => ({ type: "text", text: `browserjack: ${text}` })), + ], + }, + }); });