Skip to content

Commit c74e53d

Browse files
authored
fix: restore upstream Chrome host compatibility (#71)
1 parent 7ef8e38 commit c74e53d

8 files changed

Lines changed: 259 additions & 18 deletions

runtime/webstrap/server.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ const appHostMain = {
536536
services: {
537537
appUpdates: {
538538
setSparkleQueryParams() {},
539+
checkForUpdates() {},
539540
installUpdate() {},
540541
stateChanged(callback) {
541542
appUpdateSubscribers.add(callback);
@@ -546,6 +547,14 @@ const appHostMain = {
546547
appUpdateSubscribers.delete(callback);
547548
};
548549
}
550+
},
551+
// The browser shell has no desktop auto-resolution scheduler. Upstream
552+
// still reports renderer presentation/activity state through this service.
553+
requestUserInputAutoResolution: {
554+
setDisabled() {},
555+
snooze() {},
556+
setConversationPresented() {},
557+
recordConversationActivity() {}
549558
}
550559
}
551560
};

scripts/lib/chrome-extension-patches.mjs

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,23 @@ import { CHROME_EXTENSION_HOST_CONTENT_VARIANT } from "./chrome-extension-consta
44

55
const contractName = "Chrome plugin content-variant";
66
const materializerSignals = [
7-
".browserSkillVariant",
8-
".computerUseSkillVariant",
9-
".pluginRoot",
10-
".codex-plugin"
7+
".codex-plugin",
8+
"plugin.json"
119
];
1210

1311
export const linuxChromeExtensionHostContentVariantContract = {
1412
name: "linux-chrome-extension-host-content-variant",
1513
find: findContentVariantContract,
1614
assertBefore(source) {
1715
const match = findContentVariantContract(source);
18-
if (match.status !== "patch") throw new Error("Linux Chrome content variant is already present");
16+
if (match.status !== "patch") {
17+
throw new Error("Linux Chrome content variant does not require patching");
18+
}
1919
},
2020
apply: patchLinuxChromeExtensionHostContentVariant,
2121
assertAfter(source) {
22-
if (!hasLinuxChromeExtensionHostContentVariant(source)) {
22+
const match = findContentVariantContract(source);
23+
if (!["patched", "absent"].includes(match.status)) {
2324
throw new Error("Linux Chrome content variant was not applied");
2425
}
2526
}
@@ -29,7 +30,7 @@ function findContentVariantContract(source) {
2930
if (hasLinuxChromeExtensionHostContentVariant(source)) {
3031
return { status: "patched" };
3132
}
32-
return { status: "patch", ...findContentVariantMaterializer(source) };
33+
return findContentVariantMaterializer(source);
3334
}
3435

3536
/**
@@ -42,6 +43,10 @@ export function patchLinuxChromeExtensionHostContentVariant(source) {
4243

4344
try {
4445
const match = findContentVariantMaterializer(source);
46+
// Newer upstream builds materialize only computer-use and visualize here.
47+
// Chrome's manifest is stamped directly in chrome-plugin-patches.mjs.
48+
if (match.status === "absent") return source;
49+
4550
const variantName = source.slice(match.property.value.start, match.property.value.end);
4651
const replacement =
4752
`${match.pluginParameter}.pluginName===\`chrome\`?` +
@@ -84,9 +89,20 @@ function findContentVariantMaterializer(source) {
8489
if (!fn || pluginParameter?.type !== "Identifier") return;
8590

8691
const functionSource = source.slice(fn.start, fn.end);
87-
if (!materializerSignals.every(signal => functionSource.includes(signal))) return;
92+
const pluginParameterSignals = [
93+
`${pluginParameter.name}.pluginName`,
94+
`${pluginParameter.name}.pluginRoot`
95+
];
96+
if (
97+
![...materializerSignals, ...pluginParameterSignals].every(signal =>
98+
functionSource.includes(signal)
99+
)
100+
) {
101+
return;
102+
}
88103

89104
matches.push({
105+
functionSource,
90106
property: node,
91107
pluginParameter: pluginParameter.name
92108
});
@@ -96,7 +112,50 @@ function findContentVariantMaterializer(source) {
96112
throw contractError(`expected one runtime materializer, found ${matches.length}`);
97113
}
98114

99-
return matches[0];
115+
const [match] = matches;
116+
if (
117+
referencesParameterProperty(
118+
match.functionSource,
119+
match.pluginParameter,
120+
"browserSkillVariant"
121+
) ||
122+
hasPluginNameBranch(match.functionSource, match.pluginParameter, "chrome")
123+
) {
124+
return { status: "patch", ...match };
125+
}
126+
127+
if (isKnownNonChromeMaterializer(match.functionSource, match.pluginParameter)) {
128+
return { status: "absent", ...match };
129+
}
130+
131+
throw contractError("runtime materializer does not prove whether Chrome is handled");
132+
}
133+
134+
function isKnownNonChromeMaterializer(functionSource, pluginParameter) {
135+
return (
136+
referencesParameterProperty(
137+
functionSource,
138+
pluginParameter,
139+
"computerUseSkillVariant"
140+
) &&
141+
referencesParameterProperty(
142+
functionSource,
143+
pluginParameter,
144+
"liveVisualizationSkillVariant"
145+
) &&
146+
hasPluginNameBranch(functionSource, pluginParameter, "computer-use") &&
147+
hasPluginNameBranch(functionSource, pluginParameter, "visualize")
148+
);
149+
}
150+
151+
function referencesParameterProperty(source, parameterName, propertyName) {
152+
return source.includes(`${parameterName}.${propertyName}`);
153+
}
154+
155+
function hasPluginNameBranch(source, parameterName, pluginName) {
156+
return ["`", '"', "'"].some(quote =>
157+
source.includes(`${parameterName}.pluginName===${quote}${pluginName}${quote}`)
158+
);
100159
}
101160

102161
function isBundledContentVariantProperty(node) {

scripts/lib/chrome-extension-smoke.mjs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
33
import os from "node:os";
44
import path from "node:path";
55

6-
import { CHROME_EXTENSION_HOST_ARCH } from "./chrome-extension-constants.mjs";
6+
import {
7+
CHROME_EXTENSION_HOST_ARCH,
8+
CHROME_EXTENSION_HOST_CONTENT_VARIANT
9+
} from "./chrome-extension-constants.mjs";
710
import { chromePluginRoot } from "./chrome-extension-host.mjs";
811
import { projectRoot } from "./config.mjs";
912

@@ -22,18 +25,28 @@ export async function assertLinuxChromeExtensionHost(resourcesDir, channelName)
2225
CHROME_EXTENSION_HOST_ARCH,
2326
"extension-host"
2427
);
28+
const pluginManifestPath = path.join(pluginRoot, ".codex-plugin", "plugin.json");
2529

2630
await fs.access(hostPath).catch(error => {
2731
throw new Error(`Missing Chrome extension host: ${hostPath}`, { cause: error });
2832
});
29-
const [stat, fileType] = await Promise.all([
33+
const [stat, fileType, pluginManifestSource] = await Promise.all([
3034
fs.stat(hostPath),
31-
runFileType(hostPath)
35+
runFileType(hostPath),
36+
fs.readFile(pluginManifestPath, "utf8")
3237
]);
3338
const artifact = evaluateLinuxChromeExtensionHostArtifact({
3439
fileType,
3540
mode: stat.mode & 0o777
3641
});
42+
let pluginManifest;
43+
try {
44+
pluginManifest = evaluateLinuxChromePluginManifest(JSON.parse(pluginManifestSource));
45+
} catch (error) {
46+
throw new Error(`Invalid packaged Chrome plugin manifest: ${pluginManifestPath}`, {
47+
cause: error
48+
});
49+
}
3750
const protocol = await smokeLinuxChromeExtensionHostProtocol({
3851
channelName,
3952
hostPath,
@@ -44,10 +57,24 @@ export async function assertLinuxChromeExtensionHost(resourcesDir, channelName)
4457
return {
4558
path: path.relative(resourcesDir, hostPath),
4659
...artifact,
60+
...pluginManifest,
4761
...protocol
4862
};
4963
}
5064

65+
export function evaluateLinuxChromePluginManifest(manifest) {
66+
if (
67+
manifest?.name !== "chrome" ||
68+
manifest?.bundledContentVariant !== CHROME_EXTENSION_HOST_CONTENT_VARIANT
69+
) {
70+
throw new Error(
71+
`Chrome plugin must declare bundledContentVariant ${CHROME_EXTENSION_HOST_CONTENT_VARIANT}`
72+
);
73+
}
74+
75+
return { contentVariant: manifest.bundledContentVariant };
76+
}
77+
5178
export function evaluateLinuxChromeExtensionHostArtifact({ fileType, mode }) {
5279
if ((mode & 0o111) === 0) {
5380
throw new Error(`Chrome extension host must be executable (mode ${mode.toString(8)})`);

scripts/lib/chrome-plugin-patches.mjs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import fs from "node:fs/promises";
22
import path from "node:path";
33
import { parse } from "acorn";
44

5+
import { CHROME_EXTENSION_HOST_CONTENT_VARIANT } from "./chrome-extension-constants.mjs";
56
import { chromePluginRoot } from "./chrome-extension-host.mjs";
67

78
const nativeManifestContract = "Linux native-host manifest diagnostics";
9+
const pluginManifestVariantContract = "Linux Chrome plugin manifest variant";
810

911
export async function patchLinuxChromePluginResources(resourcesDir) {
10-
const scriptsDir = path.join(chromePluginRoot(resourcesDir), "scripts");
12+
const pluginRoot = chromePluginRoot(resourcesDir);
13+
const scriptsDir = path.join(pluginRoot, "scripts");
1114
// browser-client.mjs is SHA-pinned by the desktop runtime. Keep its bytes
1215
// intact; changing its profile metadata would disable the trusted Node REPL.
1316
const manifestCheckPath = path.join(scriptsDir, "check-native-host-manifest.js");
@@ -18,6 +21,36 @@ export async function patchLinuxChromePluginResources(resourcesDir) {
1821
});
1922
const patched = patchLinuxNativeHostManifestCheckSource(source);
2023
if (patched !== source) await fs.writeFile(manifestCheckPath, patched);
24+
25+
const pluginManifestPath = path.join(pluginRoot, ".codex-plugin", "plugin.json");
26+
const pluginManifestSource = await fs.readFile(pluginManifestPath, "utf8").catch(error => {
27+
throw new Error(`Required Chrome plugin manifest is missing: ${pluginManifestPath}`, {
28+
cause: error
29+
});
30+
});
31+
const patchedPluginManifest = patchLinuxChromePluginManifestSource(pluginManifestSource);
32+
if (patchedPluginManifest !== pluginManifestSource) {
33+
await fs.writeFile(pluginManifestPath, patchedPluginManifest);
34+
}
35+
}
36+
37+
/** Stamp the Linux host revision into the cache identity used by upstream. */
38+
export function patchLinuxChromePluginManifestSource(source) {
39+
try {
40+
const manifest = JSON.parse(source);
41+
if (manifest?.name !== "chrome") throw new Error("expected the Chrome plugin manifest");
42+
43+
const currentVariant = manifest.bundledContentVariant;
44+
if (currentVariant === CHROME_EXTENSION_HOST_CONTENT_VARIANT) return source;
45+
if (currentVariant !== undefined) {
46+
throw new Error(`unexpected bundledContentVariant ${JSON.stringify(currentVariant)}`);
47+
}
48+
49+
manifest.bundledContentVariant = CHROME_EXTENSION_HOST_CONTENT_VARIANT;
50+
return `${JSON.stringify(manifest, null, 2)}\n`;
51+
} catch (error) {
52+
throw contractError(pluginManifestVariantContract, error);
53+
}
2154
}
2255

2356
export function patchLinuxNativeHostManifestCheckSource(source) {
@@ -64,11 +97,27 @@ export function patchLinuxNativeHostManifestCheckSource(source) {
6497
}
6598

6699
function hasLinuxNativeHostManifestCheck(source) {
67-
return (
68-
source.includes('process.platform === "linux"') &&
69-
source.includes('"NativeMessagingHosts"') &&
70-
source.includes("supports macOS, Linux, and Windows")
71-
);
100+
try {
101+
const fn = findNamedFunction(source, "getNativeHostManifestLocation");
102+
const functionSource = source.slice(fn.start, fn.end);
103+
const locationShape = [
104+
'process.platform === "linux"',
105+
"manifestPath:",
106+
"registryKey: null",
107+
"registryManifestPath: null",
108+
"registryKeyExists: null"
109+
];
110+
const resolvesManifestPath =
111+
functionSource.includes('"NativeMessagingHosts"') ||
112+
functionSource.includes("resolveLinuxNativeMessagingManifestPath(");
113+
114+
return (
115+
resolvesManifestPath &&
116+
locationShape.every(signal => functionSource.includes(signal))
117+
);
118+
} catch {
119+
return false;
120+
}
72121
}
73122

74123
function findNamedFunction(source, name) {

test/chrome-extension-patches.test.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ const upstreamMaterializer = [
1717
"}"
1818
].join("");
1919

20+
const upstreamMaterializerWithoutChrome = [
21+
"async function ol(e){",
22+
"let t,n=[];",
23+
"if(e.pluginName===`computer-use`?(t=e.computerUseSkillVariant):e.pluginName===`visualize`&&(t=e.liveVisualizationSkillVariant),t==null)return;",
24+
"let r=join(e.pluginRoot,`.codex-plugin`,`plugin.json`),i=await schema.parseAsync(JSON.parse(await fs.readFile(r,`utf8`)));",
25+
"await fs.writeFile(r,`${JSON.stringify({...i,bundledContentVariant:t},null,2)}\\n`,`utf8`)",
26+
"}"
27+
].join("");
28+
2029
test("patchLinuxChromeExtensionHostContentVariant revises only the Chrome cache identity", () => {
2130
const patched = patchLinuxChromeExtensionHostContentVariant(upstreamMaterializer);
2231

@@ -45,6 +54,16 @@ test("content-variant contract runner accepts an already patched bundle", () =>
4554
);
4655
});
4756

57+
test("content-variant contract accepts a materializer without Chrome", () => {
58+
assert.equal(
59+
applyUpstreamPatchContract(
60+
upstreamMaterializerWithoutChrome,
61+
linuxChromeExtensionHostContentVariantContract
62+
),
63+
upstreamMaterializerWithoutChrome
64+
);
65+
});
66+
4867
test("patchLinuxChromeExtensionHostContentVariant fails on upstream contract drift", () => {
4968
assert.throws(
5069
() => patchLinuxChromeExtensionHostContentVariant("async function unrelated(){}"),

0 commit comments

Comments
 (0)