diff --git a/docs/upstream-dmg-intelligence.md b/docs/upstream-dmg-intelligence.md index 54dd139f9..e8f368a66 100644 --- a/docs/upstream-dmg-intelligence.md +++ b/docs/upstream-dmg-intelligence.md @@ -48,6 +48,12 @@ scripts/dev/upstream-dmg-intel.js \ --fail-on-blockers ``` +Add `--patch-preflight` to extract `app.asar` into an isolated temporary root +and prove the required window patches plus the protected Computer Use and Read +Aloud parity patches against the candidate. Raw upstream platform gates remain +visible, but become non-blocking `patched-linux-parity` entries only when every +exact owner patch reports `applied` or `already-applied`. + Explicit baseline comparison remains available for older known-good builds: ```bash @@ -81,6 +87,12 @@ Each run writes: - `plugin-map.json`: bundled plugin manifests, MCP configs, and skill files. - `native-binary-map.json`: native candidate paths, file type output when available, hashes, and protected string evidence. +- `platform-gates.json`: macOS/Windows/Linux gate inventory with Linux parity, + unsupported platform, new capability, expected platform-native, and review + classifications. +- `new-capabilities.json`: issue-candidate queue built from new plugins, MCP + tools, native binaries, bridge handlers, and platform-gated desktop feature + hints. - `map-drift.json`: baseline/candidate structural deltas for bridge handlers, plugin ids/files, MCP tools, native binaries, and Linux substrate gaps. - `drift-report.json` and `drift-report.md`: machine and human drift summaries. @@ -88,9 +100,11 @@ Each run writes: newly discovered, patch-broken, or substrate-gap surfaces. The CLI stdout summary includes `decision.acceptance`, `blockersCount`, -`reviewItemsCount`, protected-surface status counts, and whether every protected -surface is fully present. `--fail-on-blockers` exits with status `2` after -writing the report bundle when `decision.blockersCount` is nonzero. +`reviewItemsCount`, `linuxParityGateBlockersCount`, +`platformGateReviewItemsCount`, `newCapabilityIssueCandidatesCount`, +protected-surface status counts, and whether every protected surface is fully +present. `--fail-on-blockers` exits with status `2` after writing the report +bundle when `decision.blockersCount` is nonzero. When a baseline is provided, the command also writes `baseline/` and `candidate/` subdirectories with their own inventory, protected-surface, @@ -162,6 +176,32 @@ review item before accepting the upstream DMG. - `LINUX_SUBSTRATE_GAP`: upstream evidence exists, but the registry's required Linux substrate path is missing. +## Platform Gate Classifications + +The platform gate map is the first place to look when a feature exists in the +bundle but disappears from Settings, `@` mentions, menus, or plugin entrypoints. +It classifies gates separately from protected-surface drift: + +- `linux-parity-drift`: Linux already has a substrate or patch owner, but + upstream still gates the UI or query to macOS/Windows. This is a release + blocker; Computer Use belongs here. +- `patched-linux-parity`: the raw upstream gate still exists, but every exact + candidate preflight patch for its Linux surface applied. The gate remains in + the report as evidence without blocking acceptance. +- `existing-linux-feature-drift`: protected-surface movement or patch failures + for features this repo already mirrors. This remains represented by the + protected-surface classifications above. +- `new-upstream-capability`: new plugin, MCP tool, bridge, native sidecar, or + desktop feature hint that needs an issue and a Linux support decision. +- `platform-specific-unsupported`: macOS/Windows feature with no Linux substrate + yet, such as Office live-control app rows. +- `expected-platform-native`: OS-native details such as titlebar, Dock, tray, or + window chrome behavior that should be labeled but not patched by default. +- `already-linux-enabled`: a platform gate already includes Linux; keep it out + of blocker counts. +- `needs-review`: high-signal but ambiguous feature gate that must be triaged + before accepting release drift. + ## Acceptance Gate The automated tests use synthetic `.app` fixtures and `app.asar.extracted` @@ -192,3 +232,50 @@ navigation layer: registry, patch, or Linux substrate action is resolved. `PATCH_REVIEW` remains review-only unless the protected surface is also missing, partial, removed, or has a required patch failure. + +## Optional Dagger MCP + +The Dagger module is not the primary entry point. It exists as a self-hosted +agent convenience wrapper around the same devcontainer-backed tool: + +```bash +dagger functions +dagger call verify-dmg-intel +dagger call inspect-upstream-dmg-url +dagger call inspect-upstream-dmg --candidate ./Codex.dmg export --path /tmp/codex-dmg-intel-report +``` + +The `inspect-upstream-dmg-url` function downloads the current upstream DMG URL +inside Dagger, compares it to repo `Codex.dmg`, and returns a compact JSON +summary for Codex/MCP use with `decision`, `blockers`, and review-only drift. +The `inspect-upstream-dmg` function accepts a candidate DMG file and optional +baseline and patch-report files, then returns the generated report directory. +The Dagger source context ignores DMGs, build outputs, generated reports, +`target/`, and app extraction trees; pass large DMGs as explicit file +arguments instead of baking them into the module context. + +To expose it as an MCP server in Codex, add the bridge to repo-local +`.codex/config.toml` and restart the Codex session: + +```toml +[mcp_servers.codex-dmg-intel-dagger] +command = "/path/to/codex-desktop-linux/scripts/codex-dmg-intel-dagger-mcp" +``` + +The bridge also exposes Dagger-native agent functions such as +`headroom-agent-review`. By default, the launcher uses a local Headroom client +token shape with Dagger's OpenAI-compatible LLM environment and routes calls +through Headroom. Real upstream provider keys stay on the Headroom server: + +```toml +[mcp_servers.codex-dmg-intel-dagger.env] +DAGGER_HEADROOM_PROXY_URL = "http://10.10.10.89" +DAGGER_HEADROOM_MODEL = "openrouter/deepseek/deepseek-v4-flash" +DAGGER_HEADROOM_API_KEY = "headroom-local-client-token" +``` + +Use a pinned repo env only when Codex may start outside this checkout: + +```toml +CODEX_DMG_INTEL_DAGGER_REPO = "/home/kdlocpanda/second_brain/Areas/devcontainers/codex-desktop-linux" +``` diff --git a/scripts/dev/upstream-dmg-intel.js b/scripts/dev/upstream-dmg-intel.js index 1ed877cb2..d8d8d6ae7 100755 --- a/scripts/dev/upstream-dmg-intel.js +++ b/scripts/dev/upstream-dmg-intel.js @@ -24,6 +24,8 @@ Build an upstream DMG intelligence report without mutating codex-app/. Options: --candidate PATH Candidate Codex.dmg, extracted .app, or extracted app resources directory + --candidate-url URL Provenance URL for the candidate DMG + --patch-preflight Run required and protected Linux parity patches against a temporary extracted copy --baseline PATH Optional known-good baseline DMG or extracted .app; defaults to ./Codex.dmg when different --no-baseline Do a candidate-only scan even when ./Codex.dmg exists --patch-report PATH Optional patch-report.json to fold patch blockers/review items into drift-report.json @@ -40,6 +42,8 @@ function parseArgs(argv) { autoBaseline: true, baselinePath: null, candidatePath: null, + candidateUrl: null, + patchPreflight: false, outputDir: null, failOnBlockers: false, patchReportPath: null, @@ -51,6 +55,10 @@ function parseArgs(argv) { const arg = argv[index]; if (arg === "--candidate") { args.candidatePath = argv[++index]; + } else if (arg === "--candidate-url") { + args.candidateUrl = argv[++index]; + } else if (arg === "--patch-preflight") { + args.patchPreflight = true; } else if (arg === "--baseline") { args.baselinePath = argv[++index]; } else if (arg === "--no-baseline") { @@ -99,16 +107,25 @@ function buildDecision({ driftReport, protectedSurfaces }) { const surfaceDrift = driftReport.surfaceDrift ?? []; const blockers = surfaceDrift.filter((item) => BLOCKING_CLASSIFICATIONS.has(item.classification)); const reviewItems = surfaceDrift.filter((item) => !BLOCKING_CLASSIFICATIONS.has(item.classification)); + const linuxParityGateBlockers = driftReport.platformGateSummary?.blockingCount ?? 0; + const platformGateReviewItems = driftReport.platformGateSummary?.reviewCount ?? 0; + const newCapabilityIssueCandidates = driftReport.newCapabilitySummary?.issueCandidateCount ?? 0; const protectedSurfaceStatusCounts = statusCounts(protectedSurfaces.surfaces ?? []); const allProtectedSurfacesPresent = (protectedSurfaces.surfaces ?? []).length > 0 && (protectedSurfaces.surfaces ?? []).every((surface) => surface.status === "PRESENT"); - const acceptance = blockers.length > 0 ? "blocked" : (reviewItems.length > 0 ? "review" : "accepted"); + const blockerCount = blockers.length + linuxParityGateBlockers; + const reviewCount = reviewItems.length + platformGateReviewItems + newCapabilityIssueCandidates; + const acceptance = blockerCount > 0 ? "blocked" : (reviewCount > 0 ? "review" : "accepted"); return { acceptance, - blockersCount: blockers.length, - reviewItemsCount: reviewItems.length, + blockersCount: blockerCount, + reviewItemsCount: reviewCount, + protectedSurfaceBlockersCount: blockers.length, + linuxParityGateBlockersCount: linuxParityGateBlockers, + platformGateReviewItemsCount: platformGateReviewItems, + newCapabilityIssueCandidatesCount: newCapabilityIssueCandidates, allProtectedSurfacesPresent, protectedSurfaceStatusCounts, blockerClassifications: [...new Set(blockers.map((item) => item.classification))].sort(), @@ -141,6 +158,8 @@ function main(argv = process.argv.slice(2)) { registry, repoRoot, timestamp: args.timestamp, + provenance: args.candidateUrl ? { candidate: { url: args.candidateUrl } } : null, + runPatchPreflight: args.patchPreflight, }); const decision = buildDecision({ driftReport: reports.driftReport, @@ -151,6 +170,8 @@ function main(argv = process.argv.slice(2)) { outputDir: reports.outputDir, inventory: path.join(reports.outputDir, "inventory.json"), protectedSurfaces: path.join(reports.outputDir, "protected-surfaces.json"), + platformGates: path.join(reports.outputDir, "platform-gates.json"), + newCapabilities: path.join(reports.outputDir, "new-capabilities.json"), driftReport: path.join(reports.outputDir, "drift-report.json"), driftMarkdown: path.join(reports.outputDir, "drift-report.md"), substrateActionPlan: path.join(reports.outputDir, "substrate-action-plan.md"), @@ -161,7 +182,7 @@ function main(argv = process.argv.slice(2)) { process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); if (args.failOnBlockers && decision.blockersCount > 0) { console.error( - `Upstream DMG intelligence found ${decision.blockersCount} protected-surface acceptance blocker(s).`, + `Upstream DMG intelligence found ${decision.blockersCount} Linux acceptance blocker(s).`, ); return 2; } diff --git a/scripts/dev/upstream-dmg-intel.test.js b/scripts/dev/upstream-dmg-intel.test.js index ed4151802..ebd92f80c 100644 --- a/scripts/dev/upstream-dmg-intel.test.js +++ b/scripts/dev/upstream-dmg-intel.test.js @@ -11,7 +11,13 @@ const { buildIntelReports, compareProtectedSurfaces, createInventory, + createNewCapabilityMap, + createPlatformGateMap, + extractVersionMetadata, extractProtectedSurfaces, + findPostPatchIntegrityFindings, + mergeProvenance, + prepareRequiredPatchPreflightApp, renderActionPlanMarkdown, resolveBaselinePath, } = require("../lib/upstream-dmg-intel.js"); @@ -52,38 +58,6 @@ const registry = { requiredPaths: ["computer-use-linux/src/bin/codex-chrome-extension-host.rs"], }, }, - { - id: "work_louder_control_surface", - title: "Work Louder control-surface bundle hooks", - category: "native", - pathPatterns: [ - "codex-micro-service.*\\.js$", - "@worklouder/(device-kit-oai|wl-device-kit)/package\\.json$", - "(^|/)node-hid/package\\.json$", - "(^|/)node-hid/prebuilds/[^/]+/node-napi-v[0-9]+\\.node$", - ], - patchNamePatterns: ["work.*louder", "micro", "hid", "control.*surface"], - requiredEvidence: [ - { - id: "micro-service-entrypoint", - pathPatterns: ["codex-micro-service.*\\.js$"], - contentNeedles: ["@worklouder/device-kit-oai", "DeviceType", "Project2077"], - }, - { - id: "work-louder-device-kits", - pathPatterns: ["@worklouder/(device-kit-oai|wl-device-kit)"], - contentNeedles: ["@worklouder/device-kit-oai", "@worklouder/wl-device-kit", "node-hid"], - }, - { - id: "hid-runtime-package", - pathPatterns: ["(^|/)node-hid/package\\.json$", "(^|/)node-hid/prebuilds/"], - contentNeedles: ["node-hid"], - nativeBinaryPatterns: [ - "(^|/)node-hid/prebuilds/[^/]+/node-napi-v[0-9]+\\.node$", - ], - }, - ], - }, { id: "dictation_transcript_finalization", title: "Dictation transcript finalization", @@ -183,59 +157,111 @@ function writeFile(filePath, content, mode) { fs.writeFileSync(filePath, content, mode == null ? undefined : { mode }); } -function writeWorkLouderControlSurface({ asarExtracted, includeHid = true, resources }) { - const deviceKit = path.join( - resources, - "app.asar.unpacked/node_modules/@worklouder/device-kit-oai", - ); - const wlKit = path.join(deviceKit, "node_modules/@worklouder/wl-device-kit"); - const hid = path.join(wlKit, "node_modules/node-hid"); +function writeAsar(filePath, fileContents) { + const root = { files: {} }; + const payloads = []; + let offset = 0; + for (const [relativePath, content] of Object.entries(fileContents).sort(([left], [right]) => left.localeCompare(right))) { + const payload = Buffer.from(content); + const parts = relativePath.split("/"); + let files = root.files; + for (const part of parts.slice(0, -1)) { + files[part] ??= { files: {} }; + files = files[part].files; + } + files[parts.at(-1)] = { offset: String(offset), size: payload.length }; + payloads.push(payload); + offset += payload.length; + } - writeFile( - path.join(asarExtracted, ".vite/build/codex-micro-service-fixture.js"), - [ - "import { DeviceType, WLDeviceDiscovery } from '@worklouder/device-kit-oai';", - "const device = DeviceType.Project2077;", - "WLDeviceDiscovery.start(device);", - "require('node-hid');", - ].join("\n"), - ); - writeJson(path.join(deviceKit, "package.json"), { - name: "@worklouder/device-kit-oai", - dependencies: { - "@worklouder/wl-device-kit": "1.0.0", - }, - }); - writeFile( - path.join(deviceKit, "dist/index.js"), - "export { DeviceType, WLDeviceDiscovery } from '@worklouder/wl-device-kit';", - ); - writeJson(path.join(wlKit, "package.json"), { - name: "@worklouder/wl-device-kit", - dependencies: { - "node-hid": "3.1.0", - }, - }); - writeFile( - path.join(wlKit, "dist/index.js"), - [ - "const HID = require('node-hid');", - "export const DeviceType = { Project2077: 'Project2077' };", - "export class WLDeviceDiscovery { static start() { return HID; } }", - ].join(" "), - ); - if (includeHid) { - writeJson(path.join(hid, "package.json"), { - name: "node-hid", - version: "3.1.0", + const headerJson = Buffer.from(JSON.stringify(root)); + const picklePayloadSize = Math.ceil((4 + headerJson.length + 1) / 4) * 4; + const headerSize = 4 + picklePayloadSize; + const archive = Buffer.alloc(8 + headerSize + offset); + archive.writeUInt32LE(4, 0); + archive.writeUInt32LE(headerSize, 4); + archive.writeUInt32LE(picklePayloadSize, 8); + archive.writeUInt32LE(headerJson.length, 12); + headerJson.copy(archive, 16); + let payloadOffset = 8 + headerSize; + for (const payload of payloads) { + payload.copy(archive, payloadOffset); + payloadOffset += payload.length; + } + writeFile(filePath, archive); +} + +test("prepares raw app.asar contents for required patch preflight", () => + withTempDir((workspace) => { + const appDir = path.join(workspace, "Codex.app"); + const resourcesDir = path.join(appDir, "Contents/Resources"); + const asarPath = path.join(resourcesDir, "app.asar"); + const targetDir = path.join(workspace, "preflight"); + const mainSource = "let marker=`current-main-bundle`;"; + writeAsar(asarPath, { + ".vite/build/main-test.js": mainSource, + "package.json": JSON.stringify({ name: "codex-desktop" }), + }); + + prepareRequiredPatchPreflightApp({ appDir, targetDir }); + + assert.equal( + fs.readFileSync(path.join(targetDir, ".vite/build/main-test.js"), "utf8"), + mainSource, + ); + assert.equal(fs.existsSync(path.join(resourcesDir, "app.asar.extracted")), false); + assert.equal(fs.existsSync(asarPath), true); + })); + +test("inventories raw app.asar entries with normalized archive paths", () => + withTempDir((workspace) => { + const appDir = path.join(workspace, "Codex.app"); + writeAsar(path.join(appDir, "Contents/Resources/app.asar"), { + ".vite/build/main-test.js": "let marker=`current-main-bundle`;", }); + + const inventory = createInventory({ sourcePath: appDir }); + + assert.ok( + inventory.files.some( + (file) => file.relativePath === "Contents/Resources/app.asar/.vite/build/main-test.js", + ), + ); + assert.equal( + inventory.files.some((file) => file.relativePath.includes(".vite/build/.vite/build")), + false, + ); + })); + +test("default Read Aloud surface protects the current assistant render contract", () => + withTempDir((workspace) => { + const appDir = path.join(workspace, "Codex.app"); + const assetPath = path.join( + appDir, + "Contents/Resources/app.asar.extracted/webview/assets", + "app-initial~app-main~onboarding-page-zcfEkMl-.js", + ); writeFile( - path.join(hid, "prebuilds/HID-darwin-arm64/node-napi-v4.node"), - "Mach-O node-hid Project2077", - 0o755, + assetPath, + "return (0,Z.jsx)(Xa,{item:a,assistantCopyText:i,conversationId:l,renderCodeBlocksAsWritingBlocks:ge})", ); - } -} + const defaultRegistry = JSON.parse( + fs.readFileSync(path.join(__dirname, "upstream-dmg-protected-surfaces.json"), "utf8"), + ); + const inventory = createInventory({ registry: defaultRegistry, sourcePath: appDir }); + + const protectedSurfaces = extractProtectedSurfaces({ + inventory, + registry: defaultRegistry, + repoRoot: path.resolve(__dirname, "../.."), + }); + const readAloud = protectedSurfaces.surfaces.find( + (surface) => surface.id === "read_aloud_capability", + ); + + assert.equal(readAloud.status, "PRESENT"); + assert.deepEqual(readAloud.missingAnchors, []); + })); function createFixtureApp(root, variant = "baseline") { const appDir = path.join(root, `${variant}.app`); @@ -269,14 +295,6 @@ function createFixtureApp(root, variant = "baseline") { ); } - if (variant === "candidate" || variant === "missing-work-louder-hid") { - writeWorkLouderControlSurface({ - asarExtracted, - includeHid: variant !== "missing-work-louder-hid", - resources, - }); - } - if (variant !== "candidate") { writeFile( path.join(asarExtracted, "composer/transcript.js"), @@ -351,6 +369,48 @@ function createFixtureApp(root, variant = "baseline") { return appDir; } +function addSitesPlugin(appDir) { + const sitesPlugin = path.join( + appDir, + "Contents/Resources/plugins/openai-bundled/plugins/sites", + ); + writeJson(path.join(sitesPlugin, ".app.json"), { + apps: { + sites: { + id: "connector_20205bf7d4e99a89d7154bb849718324", + }, + }, + }); + writeJson(path.join(sitesPlugin, ".codex-plugin/plugin.json"), { + name: "sites", + version: "0.1.21", + description: "Build and deploy websites with Sites.", + skills: "./skills/", + apps: "./.app.json", + interface: { + displayName: "Sites", + shortDescription: "Build and deploy websites with Sites", + termsOfServiceURL: "https://openai.com/policies/chatgpt-sites-terms/", + category: "Productivity", + }, + }); + writeFile( + path.join(sitesPlugin, "skills/sites-building/SKILL.md"), + "Use Sites to build and deploy websites.", + ); +} + +function writeBundledPlugin(appDir, pluginId, manifest = {}) { + const pluginRoot = path.join(appDir, "Contents/Resources/plugins/openai-bundled/plugins", pluginId); + writeJson(path.join(pluginRoot, ".codex-plugin/plugin.json"), { + id: pluginId, + name: pluginId, + version: "1.0.0", + ...manifest, + }); + return pluginRoot; +} + function findClassification(driftReport, surfaceId, classification) { return driftReport.surfaceDrift.find( (entry) => entry.surfaceId === surfaceId && entry.classification === classification, @@ -438,62 +498,6 @@ test("marks Chronicle settings toggle surface partial when the Memory master tog ); })); -test("tracks Work Louder control-surface hooks when the service, kits, and HID runtime are bundled", () => - withTempDir((workspace) => { - const appDir = createFixtureApp(workspace, "candidate"); - const protectedSurfaces = extractProtectedSurfaces({ - inventory: createInventory({ registry, sourcePath: appDir }), - registry, - repoRoot: process.cwd(), - }); - const surface = protectedSurfaces.surfacesById.work_louder_control_surface; - - assert.equal(surface.status, "PRESENT"); - assert.equal(surface.linuxSubstrate.status, "UNKNOWN"); - assert.ok( - surface.satisfiedAnchors.some((anchor) => anchor.id === "micro-service-entrypoint"), - ); - assert.ok( - surface.satisfiedAnchors.some((anchor) => anchor.id === "work-louder-device-kits"), - ); - assert.ok( - surface.satisfiedAnchors.some((anchor) => anchor.id === "hid-runtime-package"), - ); - assert.ok( - surface.evidence.some((entry) => entry.path.includes("codex-micro-service-fixture.js")), - ); - assert.ok( - surface.requiredAnchors - .find((anchor) => anchor.id === "hid-runtime-package") - .matchedPaths.some((entryPath) => entryPath.includes("node-hid/prebuilds")), - ); - })); - -test("marks Work Louder control-surface hooks partial when the HID runtime disappears", () => - withTempDir((workspace) => { - const appDir = createFixtureApp(workspace, "missing-work-louder-hid"); - const protectedSurfaces = extractProtectedSurfaces({ - inventory: createInventory({ registry, sourcePath: appDir }), - registry, - repoRoot: process.cwd(), - }); - const surface = protectedSurfaces.surfacesById.work_louder_control_surface; - - assert.equal(surface.status, "PARTIAL"); - assert.ok( - surface.satisfiedAnchors.some((anchor) => anchor.id === "micro-service-entrypoint"), - ); - assert.ok( - surface.satisfiedAnchors.some((anchor) => anchor.id === "work-louder-device-kits"), - ); - assert.ok(surface.missingAnchors.some((anchor) => anchor.id === "hid-runtime-package")); - assert.ok( - surface.missingAnchors - .find((anchor) => anchor.id === "hid-runtime-package") - .missingNeedles.some((needle) => needle.startsWith("nativeBinary:")), - ); - })); - test("classifies protected-surface drift from baseline to candidate", () => withTempDir((workspace) => { const baselineApp = createFixtureApp(workspace, "baseline"); @@ -645,6 +649,410 @@ test("folds patch-report post-patch integrity findings into candidate-only repor assert.equal(finding.findings[0].path, "webview/assets/settings-page-patched.js"); })); +test("detects post-patch Computer Use gates left Darwin/Windows-only", () => + withTempDir((workspace) => { + const candidateApp = createFixtureApp(workspace, "candidate"); + const assetsDir = path.join(candidateApp, "Contents/Resources/webview/assets"); + writeFile( + path.join(assetsDir, "use-native-apps-current.js"), + "function zN(e){let t=(0,BN.c)(9),{enabled:n}=e,{platform:r,isLoading:i}=pi(),a=n&&(r===`macOS`||r===`windows`),o;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(o={order:`usage`},t[0]=o):o=t[0];let s;t[1]===a?s=t[2]:(s={params:o,queryConfig:{enabled:a,staleTime:m.FIVE_MINUTES,refetchOnWindowFocus:!1}},t[1]=a,t[2]=s);let c=Ne(`native-desktop-apps`,s),l;t[3]!==c||t[4]!==a?(l=a?c.data?.apps??[]:[],t[3]=c,t[4]=a,t[5]=l):l=t[5];let u=i||a&&c.isLoading,d;return t[6]!==l||t[7]!==u?(d={nativeApps:l,isLoading:u},t[6]=l,t[7]=u,t[8]=d):d=t[8],d}", + ); + writeFile( + path.join(assetsDir, "composer-computer-use-current.js"), + "function z2e(){let T=s===`macOS`||s===`windows`,E=T?U2e(C):[],D=T&&l?C.filter(Tie):[],O=T&&l?C.filter(lae):[],N=D2e({chromeAppPlugins:E,computerUsePlugin:w,microsoftExcelAppPlugins:D,microsoftPowerPointAppPlugins:O,onPluginMentionInserted:M,pluginMentionLabels:x,query:r});return N}", + ); + writeFile( + path.join(assetsDir, "computer-use-settings-current.js"), + "r===`linux`&&!v.availablePlugins.some(e=>e.plugin?.name===on||e.plugin?.id?.split(`@`)[0]===on)&&(v={...v,availablePlugins:[...v.availablePlugins,{marketplaceName:`openai-curated`,marketplacePath:y,logoPath:new URL(`computer-use-plugin-icon-linux.png`,import.meta.url).href,logoDarkPath:new URL(`computer-use-plugin-icon-linux.png`,import.meta.url).href,plugin:{id:on,name:on,installed:!0,enabled:!0}}]});", + ); + writeFile( + path.join(assetsDir, "computer-use-settings-card-current.js"), + "function Rt(){let b=flag,r=platform,O=[getApp()],F=[];if(b&&(r===`macOS`||r===`windows`))for(let e of O){if(e.plugin==null)continue;let t=e.plugin;F.push({id:e.appControlId,label:e.toggleAriaLabel,installed:t.plugin.installed,enabled:t.plugin.enabled})}return F}", + ); + writeFile( + path.join(assetsDir, "computer-use-native-icon-current.js"), + "function nI(e){let t=(0,rI.c)(10),{appPath:n}=e,{platform:r,isLoading:i}=pi(),a=(r===`macOS`||r===`windows`)&&n!=null&&n!==``,o=n??``;let u=Ne(`computer-use-native-desktop-app-icon`,l),d=a?u.data?.iconSmall??null:null;return d}", + ); + writeFile( + path.join(candidateApp, "Contents/Resources/app.asar.extracted/.vite/build/main-computer-use.js"), + "var bs=[{autoInstallOptOutKey:n.js(n.ws),installWhenMissing:!0,name:n.ws,isAvailable:({features:e,platform:t})=>(t===`darwin`||t===`linux`)&&e.computerUse,migrate:Ko}];", + ); + + const inventory = createInventory({ registry, sourcePath: candidateApp }); + + assert.equal( + findPostPatchIntegrityFindings(inventory).some((finding) => + finding.symbol.startsWith("computer-use-"), + ), + false, + ); + const findings = findPostPatchIntegrityFindings(inventory, { + includeComputerUsePlatformGates: true, + }); + assert.deepEqual( + findings.map((finding) => finding.symbol).filter((symbol) => symbol.startsWith("computer-use-")).sort(), + [ + "computer-use-composer-native-app-mentions-linux-gate", + "computer-use-native-app-icon-linux-gate", + "computer-use-native-apps-linux-gate", + "computer-use-plugin-registration-rollout-gate", + "computer-use-settings-native-app-card-linux-gate", + "computer-use-settings-synthetic-plugin-mask", + ], + ); + })); + +test("categorizes platform gates for Linux parity, unsupported features, and review candidates", () => + withTempDir((workspace) => { + const candidateApp = createFixtureApp(workspace, "candidate"); + const assetsDir = path.join(candidateApp, "Contents/Resources/webview/assets"); + writeFile( + path.join(assetsDir, "computer-use-current.js"), + "function zN(e){let {enabled:n}=e,{platform:r}=pi(),a=n&&(r===`macOS`||r===`windows`);let c=Ne(`native-desktop-apps`,{queryConfig:{enabled:a}});return {nativeApps:c.data?.apps??[],computerUsePlugin:w}}", + ); + writeFile( + path.join(assetsDir, "computer-use-settings-B1QCeMSP.js"), + "function settings(){let b=flag,r=platform,O=[getApp()],F=[];if(b&&(r===`macOS`||r===`windows`))for(let e of O){if(e.plugin==null)continue;let t=e.plugin;F.push({id:e.appControlId,label:e.toggleAriaLabel,installed:t.plugin.installed,enabled:t.plugin.enabled})}return F}", + ); + writeFile( + path.join(assetsDir, "computer-use-native-icon-current.js"), + "function nI(e){let t=(0,rI.c)(10),{appPath:n}=e,{platform:r,isLoading:i}=pi(),a=(r===`macOS`||r===`windows`)&&n!=null&&n!==``,o=n??``;let u=Ne(`computer-use-native-desktop-app-icon`,l),d=a?u.data?.iconSmall??null:null;return d}", + ); + writeFile( + path.join(assetsDir, "office-current.js"), + "function z2e(){let T=s===`macOS`||s===`windows`;return D2e({microsoftExcelAppPlugins:D,microsoftPowerPointAppPlugins:O,onPluginMentionInserted:M})}", + ); + writeFile( + path.join(assetsDir, "hotkey-current.js"), + "function hotkeys(){return process.platform===`darwin`||process.platform===`win32`?{setToggleHotkey:()=>true,syncCommandKeybindings:()=>true}:null}", + ); + writeFile( + path.join(assetsDir, "agi-skysight-supreme.js"), + "function supreme(){let ok=p===`macOS`||p===`windows`;return ok?`AGI Intelligence 9000 Skysight Supreme native desktop sidecar`:null}", + ); + writeFile( + path.join(assetsDir, "titlebar-current.js"), + "function titlebar(){return process.platform===`darwin`||process.platform===`win32`?{titleBarStyle:`hiddenInset`,trafficLightPosition:{x:12,y:12}}:{}}", + ); + + const platformGateMap = createPlatformGateMap({ + inventory: createInventory({ registry, sourcePath: candidateApp }), + }); + const byCategory = new Map(platformGateMap.gates.map((gate) => [gate.category, gate])); + + assert.ok(byCategory.get("linux-parity-drift")); + assert.equal(byCategory.get("linux-parity-drift").linuxSurfaceId, "computer_use_plugin"); + assert.ok( + platformGateMap.gates + .filter((gate) => gate.path.includes("computer-use")) + .every((gate) => gate.category === "linux-parity-drift"), + ); + assert.ok( + platformGateMap.gates.some( + (gate) => + gate.feature === "Computer Use settings native app cards" && + gate.patchTarget === "scripts/patches/impl/computer-use.js", + ), + ); + assert.ok( + platformGateMap.gates.some( + (gate) => + gate.feature === "Computer Use native app icons" && + gate.patchTarget === "scripts/patches/impl/computer-use.js", + ), + ); + assert.ok(byCategory.get("platform-specific-unsupported")); + assert.match(byCategory.get("platform-specific-unsupported").recommendation, /macOS\/Windows-only/); + assert.ok( + platformGateMap.gates.some( + (gate) => + gate.category === "platform-specific-unsupported" && + gate.feature === "Global hotkey/keybinding integration", + ), + ); + assert.ok(byCategory.get("new-upstream-capability")); + assert.match(byCategory.get("new-upstream-capability").feature, /Unmapped/); + assert.ok(byCategory.get("expected-platform-native")); + assert.equal(platformGateMap.blockingCount, 3); + })); + +test("does not treat generic Computer Use mentions as Linux parity drift without exact contracts", () => + withTempDir((workspace) => { + const candidateApp = createFixtureApp(workspace, "candidate"); + const assetsDir = path.join(candidateApp, "Contents/Resources/webview/assets"); + writeFile( + path.join(assetsDir, "computer-use-marketing-current.js"), + "function teaser(){let allowed=p===`macOS`||p===`windows`;return allowed?`computer use desktop preview`:null}", + ); + + const platformGateMap = createPlatformGateMap({ + inventory: createInventory({ registry, sourcePath: candidateApp }), + }); + const gate = platformGateMap.gates.find((entry) => + entry.path.endsWith("computer-use-marketing-current.js"), + ); + + assert.ok(gate); + assert.equal(gate.category, "needs-review"); + })); + +test("marks Computer Use platform gates covered only after exact parity patches apply", () => + withTempDir((workspace) => { + const candidateApp = createFixtureApp(workspace, "candidate"); + const assetsDir = path.join(candidateApp, "Contents/Resources/webview/assets"); + writeFile( + path.join(assetsDir, "computer-use-current.js"), + "function zN(e){let {enabled:n}=e,{platform:r}=pi(),a=n&&(r===`macOS`||r===`windows`);let c=Ne(`native-desktop-apps`,{queryConfig:{enabled:a}});return {nativeApps:c.data?.apps??[],computerUsePlugin:w}}", + ); + const patchFindings = [ + "linux-computer-use-ui-feature", + "linux-computer-use-plugin-gate", + "linux-computer-use-native-desktop-apps", + "linux-computer-use-ui-availability", + "linux-computer-use-install-flow", + ].map((name) => ({ name, status: "applied" })); + const inventory = createInventory({ registry, sourcePath: candidateApp }); + + const covered = createPlatformGateMap({ inventory, patchFindings }); + const computerUseGates = covered.gates.filter( + (gate) => gate.linuxSurfaceId === "computer_use_plugin", + ); + + assert.ok(computerUseGates.length > 0); + assert.ok(computerUseGates.every((gate) => gate.category === "patched-linux-parity")); + assert.equal(covered.blockingCount, 0); + + const failed = createPlatformGateMap({ + inventory, + patchFindings: patchFindings.map((finding) => + finding.name === "linux-computer-use-install-flow" + ? { ...finding, status: "skipped-optional" } + : finding, + ), + }); + assert.ok( + failed.gates.some( + (gate) => + gate.linuxSurfaceId === "computer_use_plugin" && + gate.category === "linux-parity-drift", + ), + ); + assert.ok(failed.blockingCount > 0); + })); + +test("maps Chronicle and Skysight platform gates to the record-and-replay Linux owners", () => + withTempDir((workspace) => { + const candidateApp = createFixtureApp(workspace, "candidate"); + const assetsDir = path.join(candidateApp, "Contents/Resources/webview/assets"); + writeFile( + path.join(assetsDir, "chronicle-settings-current.js"), + "function chronicle(){let allowed=p===`macOS`||p===`windows`;return allowed&&chronicleSidecarPresent&&chronicleSidecarProcessState&&rememberConsentAccepted?o.mutateAsync({enabled:!0}):chronicleDisable?.()}", + ); + writeFile( + path.join(assetsDir, "skysight-controls-current.js"), + "function skysight(){return process.platform===`darwin`||process.platform===`win32`?{status:`linux-record-replay-skysight-status`,snapshot:`skysight_snapshot`,tool:`event_stream_start`}:null}", + ); + + const platformGateMap = createPlatformGateMap({ + inventory: createInventory({ registry, sourcePath: candidateApp }), + }); + const chronicleGate = platformGateMap.gates.find((entry) => + entry.path.endsWith("chronicle-settings-current.js"), + ); + const skysightGate = platformGateMap.gates.find((entry) => + entry.path.endsWith("skysight-controls-current.js"), + ); + + assert.ok(chronicleGate); + assert.equal(chronicleGate.category, "linux-parity-drift"); + assert.equal(chronicleGate.feature, "Chronicle settings toggle paths"); + assert.equal(chronicleGate.patchTarget, "linux-features/record-and-replay/patch.js"); + + assert.ok(skysightGate); + assert.equal(skysightGate.category, "linux-parity-drift"); + assert.equal(skysightGate.feature, "Skysight controls and bridge"); + assert.equal( + skysightGate.patchTarget, + "linux-features/record-and-replay/patch.js and record-replay-linux/src/mcp.rs", + ); + })); + +test("keeps review-only platform gates out of new capability candidates", () => { + const capabilityMap = createNewCapabilityMap({ + mapDrift: { mode: "baselineComparison" }, + platformGateMap: { + gates: [ + { + id: "review-gate", + category: "needs-review", + confidence: "low", + feature: "Unclassified platform gate", + issueCandidate: false, + recommendation: "Review manually.", + patchTarget: "manual review", + path: "assets/review.js", + gate: "p===`macOS`||p===`windows`", + }, + { + id: "new-gate", + category: "new-upstream-capability", + confidence: "medium", + feature: "New native capability", + issueCandidate: true, + recommendation: "Create a Linux feature issue.", + patchTarget: "linux-features/new-native-capability", + path: "assets/new.js", + gate: "p===`macOS`||p===`windows`", + }, + ], + }, + }); + + assert.ok(capabilityMap.capabilities.some((capability) => capability.id === "platform-gate:new-gate")); + assert.ok(!capabilityMap.capabilities.some((capability) => capability.id === "platform-gate:review-gate")); +}); + +test("keeps framework, app-shell, plugin, and dependency binaries out of feature candidates", () => { + const capabilityMap = createNewCapabilityMap({ + mapDrift: { + mode: "baselineComparison", + nativeBinaryDrift: { + added: [ + "Contents/Frameworks/Codex Framework.framework/Versions/150.0.7871.101/Helpers/browser_crashpad_handler", + "Contents/MacOS/Codex", + "Contents/Resources/plugins/openai-bundled/plugins/chrome/extension-host/macos/arm64/ChatGPT for Chrome", + "Contents/Resources/app.asar.unpacked/node_modules/example/build/Release/example.node", + "Contents/Resources/codex-code-mode-host", + ], + }, + }, + platformGateMap: { gates: [] }, + }); + + assert.deepEqual( + capabilityMap.capabilities.filter((capability) => capability.type === "native-binary").map((capability) => capability.path), + ["Contents/Resources/codex-code-mode-host"], + ); +}); + +test("reports Sites as a cross-platform entitlement-gated capability with a staging recommendation", () => + withTempDir((workspace) => { + const baselineApp = createFixtureApp(workspace, "baseline"); + const candidateApp = createFixtureApp(workspace, "candidate"); + const outputDir = path.join(workspace, "sites-report"); + addSitesPlugin(candidateApp); + + const reports = buildIntelReports({ + baselinePath: baselineApp, + candidatePath: candidateApp, + outputDir, + registry, + repoRoot: process.cwd(), + }); + const sitesCapability = reports.newCapabilityMap.capabilities.find( + (capability) => capability.name === "Sites", + ); + const driftMarkdown = fs.readFileSync(path.join(outputDir, "drift-report.md"), "utf8"); + + assert.ok(sitesCapability); + assert.equal(sitesCapability.category, "cross-platform-entitlement-gated"); + assert.equal(sitesCapability.version, "0.1.21"); + assert.equal(sitesCapability.platformLabel, "cross-platform"); + assert.match( + sitesCapability.entitlementLabel, + /connector_20205bf7d4e99a89d7154bb849718324/, + ); + assert.equal(sitesCapability.patchTarget, "scripts/lib/bundled-plugins.sh"); + assert.match(sitesCapability.recommendation, /staging issue/i); + assert.match(driftMarkdown, /\| Sites \| 0\.1\.21 \| plugin \| cross-platform-entitlement-gated \|/); + assert.match( + driftMarkdown, + /connector_20205bf7d4e99a89d7154bb849718324/, + ); + assert.doesNotMatch(driftMarkdown, /\[object Object\]/); + assert.match(driftMarkdown, /\| --- \| --- \| --- \| --- \|/); + })); + +test("emits app, Electron, CLI, and bundled-plugin version deltas", () => { + const metadata = extractVersionMetadata([ + { + relativePath: "Contents/Resources/vendor/Info.plist", + text: "CFBundleShortVersionString 3.2.1 CFBundleVersion 99", + }, + { + relativePath: "Contents/Resources/node_modules/canvas/package.json", + text: '{"version":"3.2.1","dependencies":{"electron":"40.0.0"}}', + }, + { + relativePath: "Contents/Resources/codex", + nativeStrings: ["unrelated protocol version 2.0"], + }, + { + relativePath: "Contents/Info.plist", + text: "CFBundleShortVersionString 1.2.3 CFBundleVersion 456", + }, + { + relativePath: "package.json", + source: "asar", + text: '{"name":"openai-codex-electron","version":"26.623.141536","devDependencies":{"electron":"42.1.0"}}', + }, + { + relativePath: "Contents/Resources/codex", + nativeStrings: ["codex-cli version 0.99.1"], + }, + { + relativePath: "Contents/Resources/plugins/openai-bundled/plugins/sites/.codex-plugin/plugin.json", + text: '{"version":"0.1.21"}', + }, + ]); + + assert.equal(metadata.cfBundleShortVersionString, "1.2.3"); + assert.equal(metadata.cfBundleVersion, "456"); + assert.equal(metadata.appPackageVersion, "26.623.141536"); + assert.equal(metadata.electronVersion, "42.1.0"); + assert.equal(metadata.codexCliVersion, "0.99.1"); + assert.equal(metadata.bundledPluginVersions.sites, "0.1.21"); +}); + +test("candidate URL augments detected DMG provenance without dropping hashes", () => { + assert.deepEqual( + mergeProvenance( + { + candidate: { bytes: 123, sha256: "candidate-sha", etag: "etag", lastModified: "today", url: null }, + baseline: { bytes: 100, sha256: "baseline-sha", etag: null, lastModified: null, url: null }, + }, + { candidate: { url: "https://example.test/Codex.dmg" } }, + ), + { + candidate: { + bytes: 123, + sha256: "candidate-sha", + etag: "etag", + lastModified: "today", + url: "https://example.test/Codex.dmg", + }, + baseline: { bytes: 100, sha256: "baseline-sha", etag: null, lastModified: null, url: null }, + }, + ); +}); + +test("production registry protects Sites and exact remote-mobile contracts", () => { + const productionRegistry = JSON.parse( + fs.readFileSync(path.join(process.cwd(), "scripts/dev/upstream-dmg-protected-surfaces.json"), "utf8"), + ); + const sites = productionRegistry.surfaces.find((surface) => surface.id === "sites_plugin"); + const remoteMobile = productionRegistry.surfaces.find((surface) => surface.id === "remote_mobile_control"); + + assert.ok(sites); + assert.deepEqual(sites.pluginIds, ["sites"]); + assert.ok(sites.linuxSubstrate.requiredPaths.includes("scripts/lib/bundled-plugins.sh")); + assert.ok(remoteMobile); + assert.ok(remoteMobile.contentNeedles.includes("set-remote-control-connections-enabled")); + assert.ok(remoteMobile.contentNeedles.includes("remote_control_connections")); + assert.ok(!remoteMobile.contentNeedles.includes("mobile")); + assert.ok(!remoteMobile.contentNeedles.includes("control")); +}); + test("keeps drift report evidence compact and marks hashed asset churn", () => { const baseline = { source: { path: "baseline.app" }, @@ -812,6 +1220,8 @@ printf '00000000 T _SkyComputerUseClient\\n' "bridge-map.json", "plugin-map.json", "native-binary-map.json", + "platform-gates.json", + "new-capabilities.json", "map-drift.json", "drift-report.json", "drift-report.md", @@ -833,9 +1243,16 @@ printf '00000000 T _SkyComputerUseClient\\n' const nativeBinaryMap = JSON.parse( fs.readFileSync(path.join(reports.outputDir, "native-binary-map.json"), "utf8"), ); + const platformGateMap = JSON.parse( + fs.readFileSync(path.join(reports.outputDir, "platform-gates.json"), "utf8"), + ); + const newCapabilityMap = JSON.parse( + fs.readFileSync(path.join(reports.outputDir, "new-capabilities.json"), "utf8"), + ); const mapDrift = JSON.parse( fs.readFileSync(path.join(reports.outputDir, "map-drift.json"), "utf8"), ); + const driftMarkdown = fs.readFileSync(path.join(reports.outputDir, "drift-report.md"), "utf8"); const actionPlan = fs.readFileSync(path.join(reports.outputDir, "substrate-action-plan.md"), "utf8"); const skyDrift = findClassification(driftReport, "sky_computer_use_client", "MOVED"); assert.ok(findClassification(driftReport, "chronicle_sidecar", "NEW_UPSTREAM_CAPABILITY")); @@ -845,6 +1262,10 @@ printf '00000000 T _SkyComputerUseClient\\n' assert.ok(skyDrift.evidenceDrift.addedPathSamples.length > 0); assert.equal(skyDrift.evidenceDrift.addedEvidence, undefined); assert.ok(inventory.files.every((file) => file.text == null && file.nativeStrings == null)); + assert.ok(Array.isArray(platformGateMap.gates)); + assert.ok(newCapabilityMap.capabilities.some((capability) => capability.type === "mcp-tool")); + assert.match(driftMarkdown, /## New Capability Candidates/); + assert.match(driftMarkdown, /## Linux Parity Drift/); assert.ok( nativeBinaryMap.binaries.some((binary) => binary.protectedStringHits.some((hit) => hit.needle === "recording_controls"), @@ -942,7 +1363,6 @@ test("CLI loads the checked-in registry and writes the report bundle", () => assert.equal(protectedSurfaces.surfacesById.record_and_replay_plugin.status, "PRESENT"); assert.equal(protectedSurfaces.surfacesById.codex_chronicle.status, "PRESENT"); assert.equal(protectedSurfaces.surfacesById.chronicle_settings_toggles.status, "PRESENT"); - assert.equal(protectedSurfaces.surfacesById.work_louder_control_surface.status, "PRESENT"); })); test("CLI exits nonzero with --fail-on-blockers when acceptance blockers are present", () => @@ -968,7 +1388,7 @@ test("CLI exits nonzero with --fail-on-blockers when acceptance blockers are pre const summary = JSON.parse(result.stdout); assert.equal(summary.decision.acceptance, "blocked"); assert.ok(summary.decision.blockersCount > 0); - assert.match(result.stderr, /protected-surface acceptance blocker/); + assert.match(result.stderr, /Linux acceptance blocker/); assert.ok(fs.existsSync(path.join(outputDir, "drift-report.json"))); })); diff --git a/scripts/dev/upstream-dmg-protected-surfaces.json b/scripts/dev/upstream-dmg-protected-surfaces.json index 449ebe859..7690933fd 100644 --- a/scripts/dev/upstream-dmg-protected-surfaces.json +++ b/scripts/dev/upstream-dmg-protected-surfaces.json @@ -2,6 +2,57 @@ "version": 1, "description": "Protected upstream Codex Desktop macOS DMG surfaces that Linux packaging mirrors, patches, stages, or must explicitly classify before accepting a new upstream build.", "surfaces": [ + { + "id": "read_aloud_capability", + "title": "Read Aloud capability and staging contract", + "category": "feature", + "pathPatterns": ["webview/assets/app-initial~app-main~onboarding-page-[A-Za-z0-9_-]+\\.js$"], + "contentNeedles": ["assistantCopyText", "conversationId", "renderCodeBlocksAsWritingBlocks"], + "patchNamePatterns": ["read.*aloud"], + "requiredEvidence": [ + { + "id": "current-assistant-render-contract", + "pathPatterns": ["webview/assets/app-initial~app-main~onboarding-page-[A-Za-z0-9_-]+\\.js$"], + "contentNeedles": ["assistantCopyText", "conversationId", "renderCodeBlocksAsWritingBlocks"] + } + ], + "linuxSubstrate": {"requiredPaths": ["linux-features/read-aloud", "linux-features/read-aloud/patch.js"]} + }, + { + "id": "sites_plugin", + "title": "Sites plugin manifest, connector registration, and Linux staging", + "category": "plugin", + "pathPatterns": ["plugins/openai-bundled/plugins/sites/"], + "contentNeedles": ["connector_20205bf7d4e99a89d7154bb849718324", "chatgpt-sites-terms"], + "pluginIds": ["sites"], + "patchNamePatterns": ["sites", "bundled.*plugin"], + "requiredEvidence": [ + { + "id": "sites-plugin-manifest", + "pathPatterns": ["plugins/openai-bundled/plugins/sites/\\.codex-plugin/plugin\\.json$"], + "pluginIds": ["sites"] + }, + { + "id": "sites-connector-registration", + "pathPatterns": ["plugins/openai-bundled/plugins/sites/\\.app\\.json$"], + "contentNeedles": ["connector_20205bf7d4e99a89d7154bb849718324"] + } + ], + "linuxSubstrate": {"requiredPaths": ["scripts/lib/bundled-plugins.sh"]} + }, + { + "id": "remote_mobile_control", + "title": "Remote mobile control UI, bridge, and staging contract", + "category": "feature", + "pathPatterns": ["remote-mobile-control", "remote-control-connections", "remote-connections-settings"], + "contentNeedles": [ + "remote_control_connections", + "set-remote-control-connections-enabled", + "shouldLoadRemoteControlConnections" + ], + "patchNamePatterns": ["remote.*mobile", "mobile.*control"], + "linuxSubstrate": {"requiredPaths": ["linux-features/remote-mobile-control", "linux-features/remote-mobile-control/stage.sh"]} + }, { "id": "codex_chronicle", "title": "Chronicle sidecar binary and event bundle schema", diff --git a/scripts/lib/upstream-dmg-intel.js b/scripts/lib/upstream-dmg-intel.js index 1f103e46a..a4958281a 100644 --- a/scripts/lib/upstream-dmg-intel.js +++ b/scripts/lib/upstream-dmg-intel.js @@ -6,7 +6,7 @@ const os = require("node:os"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); -const TEXT_FILE_PATTERN = /\.(cjs|css|html|js|json|mjs|md|text|ts|tsx|txt|xml|yml|yaml)$/i; +const TEXT_FILE_PATTERN = /\.(cjs|css|html|js|json|mjs|md|plist|text|ts|tsx|txt|xml|yml|yaml)$/i; const NATIVE_FILE_PATTERN = /(^|\/)(codex_chronicle|SkyComputerUseClient|sky\.node|node_repl|node|[^/]+\.(node|dylib))$/i; const DEFAULT_MAX_TEXT_BYTES = 2_500_000; const DEFAULT_MAX_INVENTORY_HASH_BYTES = 10_000_000; @@ -29,6 +29,23 @@ const ACTIONABLE_CLASSIFICATIONS = new Set([ "PROTECTED_SURFACE_PARTIAL", "PROTECTED_SURFACE_MISSING", ]); +const PLATFORM_GATE_BLOCKING_CATEGORIES = new Set(["linux-parity-drift"]); +const PLATFORM_GATE_REVIEW_CATEGORIES = new Set([ + "new-upstream-capability", + "platform-specific-unsupported", + "needs-review", +]); +const LINUX_PARITY_SURFACE_PATCHES = { + computer_use_plugin: [ + "linux-computer-use-ui-feature", + "linux-computer-use-plugin-gate", + "linux-computer-use-native-desktop-apps", + "linux-computer-use-ui-availability", + "linux-computer-use-install-flow", + ], +}; +const PLATFORM_GATE_MARKDOWN_LIMIT = 20; +const NEW_CAPABILITY_MARKDOWN_LIMIT = 20; const STRING_LITERAL_PATTERN = /`([^`\\]*(?:\\.[^`\\]*)*)`|"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/g; const BRIDGE_CHANNEL_TERM_PATTERN = /browser[-_]trace|chrome|chronicle|computer[-_]use|desktop[-_]snapshot|dictation|event[-_]stream|focused[-_]window|global[-_]dictation|nativeMessaging|record[-_ ]?(?:and[-_ ]?)?replay|skysight|speech[-_]context|window[-_]metadata/i; @@ -41,6 +58,22 @@ function sha256(buffer) { return crypto.createHash("sha256").update(buffer).digest("hex"); } +function sha256File(filePath) { + const hash = crypto.createHash("sha256"); + const buffer = Buffer.allocUnsafe(1024 * 1024); + const fd = fs.openSync(filePath, "r"); + try { + let bytesRead; + do { + bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null); + if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead)); + } while (bytesRead > 0); + } finally { + fs.closeSync(fd); + } + return hash.digest("hex"); +} + function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } @@ -50,6 +83,136 @@ function writeJson(filePath, value) { fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } +const REQUIRED_PATCH_PREFLIGHTS = new Set([ + "linux-window-options", + "linux-native-titlebar", + "linux-avatar-overlay-mouse-passthrough", + "linux-tray", + "main-process-ui", +]); +const LINUX_PARITY_PATCH_PREFLIGHTS = new Set([ + "linux-computer-use-ui-feature", + "linux-computer-use-plugin-gate", + "linux-computer-use-native-desktop-apps", + "linux-computer-use-ui-availability", + "linux-computer-use-install-flow", + "feature:read-aloud:main-handler", + "feature:read-aloud:assistant-runtime", + "feature:read-aloud:settings-toggle", + "feature:read-aloud-mcp:linux-read-aloud-plugin-gate", +]); +const PATCH_PREFLIGHTS = new Set([ + ...REQUIRED_PATCH_PREFLIGHTS, + ...LINUX_PARITY_PATCH_PREFLIGHTS, +]); + +function patchPreflightOwner(name) { + if (name === "main-process-ui") { + return "scripts/patches/runner.js"; + } + if (name.startsWith("linux-computer-use")) { + return "scripts/patches/impl/computer-use.js"; + } + if (name.includes("read-aloud")) { + return name.startsWith("feature:read-aloud-mcp") + ? "linux-features/read-aloud-mcp/patches.js" + : "linux-features/read-aloud/patch.js"; + } + return "scripts/patches/core/all-linux/main-process/window-shell/patch.js"; +} + +function patchPreflightSurfaceId(name) { + if (name.startsWith("linux-computer-use")) { + return "computer_use_plugin"; + } + if (name.includes("read-aloud")) { + return "read_aloud_capability"; + } + return null; +} + +function runRequiredPatchPreflight({ inventory, repoRoot, workDir } = {}) { + const result = { + status: "not-run", + evidenceType: "safe-extracted-candidate-patch-preflight", + sourcePath: inventory?.source?.appDir ?? null, + findings: [], + }; + if (!inventory?.source?.appDir || !fs.existsSync(path.join(repoRoot, "scripts/patch-linux-window-ui.js"))) { + result.status = "unknown"; + result.reason = "patch preflight entrypoint unavailable"; + return result; + } + const extractedCopy = path.join(workDir, "required-patch-preflight-app"); + try { + prepareRequiredPatchPreflightApp({ + appDir: inventory.source.appDir, + targetDir: extractedCopy, + }); + } catch (error) { + result.status = "blocked"; + result.reason = error.message; + result.findings = [...PATCH_PREFLIGHTS].map((name) => ({ + name, + status: "failed-required", + severity: "blocker", + ownerPath: patchPreflightOwner(name), + surfaceId: patchPreflightSurfaceId(name), + recommendation: "Restore candidate extraction before package build.", + reason: error.message, + })); + return result; + } + const featuresConfigPath = path.join(workDir, "required-patch-preflight-features.json"); + writeJson(featuresConfigPath, { enabled: ["read-aloud", "read-aloud-mcp"] }); + const reportPath = path.join(workDir, "required-patch-preflight.json"); + const command = spawnSync(process.execPath, [ + path.join(repoRoot, "scripts/patch-linux-window-ui.js"), + "--report-json", reportPath, + "--enforce-critical", + extractedCopy, + ], { + encoding: "utf8", + env: { + ...process.env, + CODEX_LINUX_ENABLE_COMPUTER_USE_UI: "1", + CODEX_LINUX_FEATURES_CONFIG: featuresConfigPath, + CODEX_LINUX_FEATURES_ROOT: path.join(repoRoot, "linux-features"), + HOME: path.join(workDir, "home"), + }, + maxBuffer: 4 * 1024 * 1024, + }); + const report = fs.existsSync(reportPath) ? readJson(reportPath) : { patches: [] }; + const patches = (report.patches ?? []).filter((entry) => PATCH_PREFLIGHTS.has(entry.name)); + result.status = command.status === 0 && [...PATCH_PREFLIGHTS].every((name) => + patches.some((entry) => entry.name === name && ["applied", "already-applied"].includes(entry.status))) + ? "pass" + : "blocked"; + result.exitCode = command.status; + result.findings = patches.map((entry) => ({ + name: entry.name, + status: entry.status, + severity: ["applied", "already-applied"].includes(entry.status) ? "none" : "blocker", + ownerPath: patchPreflightOwner(entry.name), + surfaceId: patchPreflightSurfaceId(entry.name), + recommendation: ["applied", "already-applied"].includes(entry.status) ? "No action." : "Restore the exact current-bundle patch contract before package build.", + reason: entry.reason ?? null, + })); + for (const name of PATCH_PREFLIGHTS) { + if (!patches.some((entry) => entry.name === name)) { + result.findings.push({ + name, + status: "not-recorded", + severity: "blocker", + ownerPath: patchPreflightOwner(name), + surfaceId: patchPreflightSurfaceId(name), + recommendation: "Record and resolve this required patch before package build.", + }); + } + } + return result; +} + function normalizePath(value) { return String(value).replace(/\\/g, "/").replace(/^\/+/, ""); } @@ -106,6 +269,7 @@ function printableStrings(buffer, minLength = 4) { } function asarEntries(asarPath, prefix = "app.asar") { + const archivePrefix = prefix; const archive = fs.readFileSync(asarPath); if (archive.length < 16) { throw new Error(`Invalid ASAR archive: ${asarPath}`); @@ -117,9 +281,9 @@ function asarEntries(asarPath, prefix = "app.asar") { const dataStart = 8 + headerSize; const entries = []; - function walk(prefix, files) { + function walk(directory, files) { for (const [name, entry] of Object.entries(files ?? {})) { - const fullPath = prefix ? `${prefix}/${name}` : name; + const fullPath = directory ? `${directory}/${name}` : name; if (entry.files) { walk(fullPath, entry.files); } else { @@ -128,8 +292,11 @@ function asarEntries(asarPath, prefix = "app.asar") { const unpacked = Boolean(entry.unpacked); const buffer = unpacked ? null : archive.subarray(dataStart + offset, dataStart + offset + size); entries.push({ + archivePath: fullPath, buffer, - relativePath: `${prefix}/${fullPath}`, + executable: Boolean(entry.executable), + link: entry.link ?? null, + relativePath: normalizePath(path.posix.join(archivePrefix, fullPath)), size, source: "asar", unpacked, @@ -142,6 +309,70 @@ function asarEntries(asarPath, prefix = "app.asar") { return entries; } +function asarDestinationPath(targetDir, archivePath) { + const normalized = normalizePath(archivePath); + if (normalized.length === 0 || normalized === ".." || normalized.startsWith("../")) { + throw new Error(`Unsafe ASAR entry path: ${archivePath}`); + } + const targetRoot = path.resolve(targetDir); + const destination = path.resolve(targetRoot, ...normalized.split("/")); + if (destination !== targetRoot && !destination.startsWith(`${targetRoot}${path.sep}`)) { + throw new Error(`Unsafe ASAR entry path: ${archivePath}`); + } + return destination; +} + +function extractAsarToDirectory(asarPath, targetDir) { + fs.mkdirSync(targetDir, { recursive: true }); + for (const entry of asarEntries(asarPath, "")) { + if (entry.link != null) { + continue; + } + const destination = asarDestinationPath(targetDir, entry.archivePath); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + if (entry.unpacked) { + const unpackedSource = path.join(`${asarPath}.unpacked`, ...entry.archivePath.split("/")); + if (fs.existsSync(unpackedSource)) { + fs.copyFileSync(unpackedSource, destination); + } + } else if (entry.buffer != null) { + fs.writeFileSync(destination, entry.buffer, entry.executable ? { mode: 0o755 } : undefined); + } + } +} + +function copyDirectoryContents(sourceDir, targetDir) { + fs.mkdirSync(targetDir, { recursive: true }); + for (const entry of fs.readdirSync(sourceDir)) { + fs.cpSync(path.join(sourceDir, entry), path.join(targetDir, entry), { recursive: true }); + } +} + +function prepareRequiredPatchPreflightApp({ appDir, targetDir } = {}) { + if (appDir == null || targetDir == null) { + throw new Error("appDir and targetDir are required for patch preflight"); + } + fs.rmSync(targetDir, { force: true, recursive: true }); + const resourcesDir = fs.existsSync(path.join(appDir, "Contents/Resources")) + ? path.join(appDir, "Contents/Resources") + : appDir; + const extractedAsarDir = path.join(resourcesDir, "app.asar.extracted"); + const asarPath = path.join(resourcesDir, "app.asar"); + if (fs.existsSync(extractedAsarDir)) { + copyDirectoryContents(extractedAsarDir, targetDir); + return targetDir; + } + if (fs.existsSync(asarPath)) { + extractAsarToDirectory(asarPath, targetDir); + return targetDir; + } + if (fs.existsSync(path.join(appDir, ".vite/build"))) { + copyDirectoryContents(appDir, targetDir); + return targetDir; + } + throw new Error(`Could not find app.asar or extracted app contents under ${appDir}`); +} + function walkFiles(rootDir, source = "filesystem", prefix = "") { const files = []; const stack = [rootDir]; @@ -393,6 +624,7 @@ function createInventory({ registry = null, sourcePath, workDir = null } = {}) { textFiles: files.filter((file) => file.type === "text" || file.type === "json").length, }, files, + versionMetadata: extractVersionMetadata(files), }; } finally { if (cleanupScratch && scratchDir != null) { @@ -401,9 +633,117 @@ function createInventory({ registry = null, sourcePath, workDir = null } = {}) { } } +function firstVersion(value) { + const match = String(value ?? "").match(/\b\d+\.\d+(?:\.\d+){0,2}(?:[-+][0-9A-Za-z.-]+)?\b/); + return match?.[0] ?? null; +} + +function parseJsonObject(text) { + try { + const parsed = JSON.parse(text); + return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +} + +function canonicalPlistVersions(file) { + const values = { + cfBundleShortVersionString: null, + cfBundleVersion: null, + }; + const text = file.text ?? ""; + for (const [field, key] of Object.entries({ + cfBundleShortVersionString: "CFBundleShortVersionString", + cfBundleVersion: "CFBundleVersion", + })) { + values[field] = + text.match(new RegExp(`\\s*${key}\\s*\\s*\\s*([^<]+)`, "i"))?.[1]?.trim() ?? + text.match(new RegExp(`${key}[^0-9]{0,20}([0-9][0-9A-Za-z.+-]*)`, "i"))?.[1] ?? + null; + } + if ((values.cfBundleShortVersionString != null && values.cfBundleVersion != null) || file.absolutePath == null) { + return values; + } + const script = [ + "import json, plistlib, sys", + "with open(sys.argv[1], 'rb') as handle:", + " data = plistlib.load(handle)", + "print(json.dumps({key: data.get(key) for key in ('CFBundleShortVersionString', 'CFBundleVersion')}))", + ].join("\n"); + const parsed = spawnSync("python3", ["-c", script, file.absolutePath], { + encoding: "utf8", + maxBuffer: 1024 * 1024, + }); + if (parsed.status === 0) { + const plist = parseJsonObject(parsed.stdout); + values.cfBundleShortVersionString ??= firstVersion(plist?.CFBundleShortVersionString); + values.cfBundleVersion ??= String(plist?.CFBundleVersion ?? "").trim() || null; + } + return values; +} + +function extractVersionMetadata(files = []) { + const result = { + cfBundleShortVersionString: null, + cfBundleVersion: null, + appPackageVersion: null, + electronVersion: null, + codexCliVersion: null, + bundledPluginVersions: {}, + evidence: [], + }; + const record = (key, value, evidencePath) => { + if (value != null && result[key] == null) { + result[key] = value; + result.evidence.push({ field: key, path: evidencePath }); + } + }; + for (const file of files) { + const relativePath = normalizePath(file.relativePath); + const pathLower = relativePath.toLowerCase(); + const text = file.text ?? (file.nativeStrings ?? []).join(" "); + if (!text) continue; + if (pathLower === "contents/info.plist") { + const plistVersions = canonicalPlistVersions(file); + record("cfBundleShortVersionString", plistVersions.cfBundleShortVersionString, relativePath); + record("cfBundleVersion", plistVersions.cfBundleVersion, relativePath); + } + if ( + pathLower === "contents/resources/app.asar/package.json" || + (pathLower === "package.json" && file.source === "asar") + ) { + const manifest = parseJsonObject(text); + if (manifest?.name === "openai-codex-electron" || manifest?.productName === "Codex") { + record("appPackageVersion", firstVersion(manifest.version), relativePath); + record( + "electronVersion", + firstVersion(manifest.devDependencies?.electron ?? manifest.dependencies?.electron), + relativePath, + ); + } + } + if (pathLower === "contents/resources/codex" || pathLower === "contents/resources/codex.exe") { + const cliVersion = text.match( + /\bcodex-cli(?:\s+version)?\s*[:=]?\s*v?(\d+\.\d+(?:\.\d+){0,2}(?:[-+][0-9A-Za-z.-]+)?)/i, + )?.[1]; + record("codexCliVersion", firstVersion(cliVersion), relativePath); + } + const pluginMatch = relativePath.match(/plugins\/openai-bundled\/plugins\/([^/]+)\/\.codex-plugin\/plugin\.json$/); + if (pluginMatch) { + const version = firstVersion(parseJsonObject(text)?.version); + if (version != null) result.bundledPluginVersions[pluginMatch[1]] = version; + } + } + return result; +} + function fileEvidenceForSurface(inventory, surface) { const evidence = []; for (const file of inventory.files) { + if (/(^|\/)(legacy|fallback|previous-bundle)(\/|$)/i.test(file.relativePath)) { + continue; + } const pathHit = matchAny(surface.pathPatterns, file.relativePath); const contentHits = []; const nativeHits = []; @@ -1005,7 +1345,122 @@ function hasLocalPatchSymbolDeclaration(source, symbol) { ); } -function findPostPatchIntegrityFindings(inventory) { +function findComputerUsePlatformGateFindings(inventory) { + const findings = []; + const nativeAppsQueryGatePattern = + /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&\(([A-Za-z_$][\w$]*)===`macOS`\|\|\3===`windows`(?!\|\|\3===`linux`)\)/g; + const nativeAppMentionSectionPattern = + /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)===`macOS`\|\|\2===`windows`(?!\|\|\2===`linux`)/g; + const nativeAppSettingsCardPattern = + /if\(([A-Za-z_$][\w$]*)&&\(([A-Za-z_$][\w$]*)===`macOS`\|\|\2===`windows`(?!\|\|\2===`linux`)\)\)for\(let ([A-Za-z_$][\w$]*) of ([A-Za-z_$][\w$]*)\)\{/g; + const nativeAppIconQueryGatePattern = + /([A-Za-z_$][\w$]*)=\(([A-Za-z_$][\w$]*)===`macOS`\|\|\2===`windows`(?!\|\|\2===`linux`)\)&&([A-Za-z_$][\w$]*)!=null&&\3!==``/g; + const pluginRegistrationRolloutGatePattern = + /(?:isEnabled|isAvailable):\(\{features:([A-Za-z_$][\w$]*),platform:([A-Za-z_$][\w$]*)\}\)=>\(\2===`darwin`\|\|\2===`linux`\)&&\1\.computerUse/g; + for (const file of inventory.files) { + if (file.text == null) { + continue; + } + if (file.text.includes("`native-desktop-apps`") && file.text.includes("nativeApps:")) { + for (const match of file.text.matchAll(nativeAppsQueryGatePattern)) { + const nextSource = file.text.slice(match.index + match[0].length, match.index + match[0].length + 1400); + if (!nextSource.includes("`native-desktop-apps`") || !nextSource.includes("nativeApps:")) { + continue; + } + findings.push({ + path: file.relativePath, + reason: "Computer Use native app query is still gated to macOS/Windows after Linux patching", + snippet: textSnippet(file.text, match[0]), + symbol: "computer-use-native-apps-linux-gate", + }); + } + } + if ( + file.text.includes("computerUsePlugin:") && + file.text.includes("chromeAppPlugins:") && + file.text.includes("microsoftExcelAppPlugins:") + ) { + for (const match of file.text.matchAll(nativeAppMentionSectionPattern)) { + const nextSource = file.text.slice(match.index + match[0].length, match.index + match[0].length + 1600); + if ( + !nextSource.includes("computerUsePlugin:") || + !nextSource.includes("chromeAppPlugins:") || + !nextSource.includes("microsoftExcelAppPlugins:") + ) { + continue; + } + findings.push({ + path: file.relativePath, + reason: "Computer Use composer native-app mention section is still gated to macOS/Windows after Linux patching", + snippet: textSnippet(file.text, match[0]), + symbol: "computer-use-composer-native-app-mentions-linux-gate", + }); + } + } + if ( + file.text.includes("appControlId") && + file.text.includes("toggleAriaLabel") && + file.text.includes("plugin.installed") + ) { + for (const match of file.text.matchAll(nativeAppSettingsCardPattern)) { + const nextSource = file.text.slice(match.index + match[0].length, match.index + match[0].length + 1800); + if ( + !nextSource.includes("appControlId") || + !nextSource.includes("toggleAriaLabel") || + !nextSource.includes("plugin.installed") + ) { + continue; + } + findings.push({ + path: file.relativePath, + reason: "Computer Use settings native app cards are still gated to macOS/Windows after Linux patching", + snippet: textSnippet(file.text, match[0]), + symbol: "computer-use-settings-native-app-card-linux-gate", + }); + } + } + if (file.text.includes("`computer-use-native-desktop-app-icon`")) { + for (const match of file.text.matchAll(nativeAppIconQueryGatePattern)) { + const nextSource = file.text.slice(match.index + match[0].length, match.index + match[0].length + 1200); + if (!nextSource.includes("`computer-use-native-desktop-app-icon`")) { + continue; + } + findings.push({ + path: file.relativePath, + reason: "Computer Use native app icon query is still gated to macOS/Windows after Linux patching", + snippet: textSnippet(file.text, match[0]), + symbol: "computer-use-native-app-icon-linux-gate", + }); + } + } + if (file.text.includes("installWhenMissing:!0")) { + for (const match of file.text.matchAll(pluginRegistrationRolloutGatePattern)) { + findings.push({ + path: file.relativePath, + reason: "Linux Computer Use plugin registration still depends on the upstream rollout flag", + snippet: textSnippet(file.text, match[0]), + symbol: "computer-use-plugin-registration-rollout-gate", + }); + } + } + if ( + file.text.includes("computer-use-plugin-icon-linux.png") && + file.text.includes("availablePlugins.some") && + file.text.includes("plugin?.name") && + !file.text.includes("e.plugin?.installed===!0&&e.plugin?.enabled===!0") + ) { + findings.push({ + path: file.relativePath, + reason: "Synthetic Linux Computer Use settings plugin can be masked by an unavailable upstream entry", + snippet: textSnippet(file.text, "computer-use-plugin-icon-linux.png"), + symbol: "computer-use-settings-synthetic-plugin-mask", + }); + } + } + return findings; +} + +function findPostPatchIntegrityFindings(inventory, options = {}) { const findings = []; for (const file of inventory.files) { if (file.text == null) { @@ -1024,9 +1479,358 @@ function findPostPatchIntegrityFindings(inventory) { }); } } + if (options.includeComputerUsePlatformGates === true) { + findings.push(...findComputerUsePlatformGateFindings(inventory)); + } return findings.sort((a, b) => `${a.symbol}:${a.path}`.localeCompare(`${b.symbol}:${b.path}`)); } +function platformGateContext(file, index, matchLength, radius = 900) { + const start = Math.max(0, index - radius); + const end = Math.min(file.text.length, index + matchLength + radius); + return file.text.slice(start, end); +} + +function compactSnippet(value) { + return String(value).replace(/\s+/g, " ").trim().slice(0, 260); +} + +function isComputerUsePlatformGateContext(lower, pathLower) { + return ( + (pathLower.includes("computer-use") && !pathLower.includes("marketing")) || + lower.includes("computer-use-native-desktop-app-icon") || + lower.includes("computer-use-plugin-icon-linux") || + (lower.includes("native-desktop-apps") && + (lower.includes("availableplugins") || lower.includes("plugin.installed") || lower.includes("appcontrolid") || + lower.includes("queryconfig") || lower.includes("computeruseplugin"))) || + (lower.includes("accessibility_snapshot") && lower.includes("event_stream")) + ); +} + +function classifyPlatformGate({ file, gate, context }) { + const lower = context.toLowerCase(); + const pathLower = file.relativePath.toLowerCase(); + + if (gate.includes("`linux`")) { + return { + category: "already-linux-enabled", + confidence: "high", + feature: "Linux-aware platform gate", + issueCandidate: false, + linuxSurfaceId: null, + patchTarget: "none", + recommendation: "No action; Linux is already part of this platform gate.", + }; + } + + const chronicleContract = + (lower.includes("chroniclesidecarpresent") && lower.includes("chroniclesidecarprocessstate") && + lower.includes("rememberconsentaccepted")) || + (lower.includes("skysight_snapshot") && lower.includes("event_stream_start")) || + (lower.includes("linux-record-replay-skysight-status") && lower.includes("skysight")); + if (chronicleContract) { + const skysight = lower.includes("skysight") && !lower.includes("chroniclesidecar"); + return { + category: "linux-parity-drift", + confidence: "high", + confidenceRationale: "Exact Chronicle/Skysight status, consent, or bridge contract matched.", + feature: skysight ? "Skysight controls and bridge" : "Chronicle settings toggle paths", + issueCandidate: false, + linuxSurfaceId: skysight ? "skysight_bridge" : "chronicle_settings_toggles", + patchTarget: skysight + ? "linux-features/record-and-replay/patch.js and record-replay-linux/src/mcp.rs" + : "linux-features/record-and-replay/patch.js", + linuxStatus: "existing-linux-substrate", + recommendation: "Expose and test the explicit stopped/not-started state, consent, enable/disable, and bridge status semantics on Linux.", + }; + } + + if ( + lower.includes("appcontrolid") && + lower.includes("togglearialabel") && + lower.includes("plugin.installed") + ) { + return { + category: "linux-parity-drift", + confidence: "high", + confidenceRationale: "Exact Computer Use settings card contract matched.", + feature: "Computer Use settings native app cards", + issueCandidate: false, + linuxSurfaceId: "computer_use_plugin", + patchTarget: "scripts/patches/impl/computer-use.js", + recommendation: "Patch the settings native app card loop so Linux renders the existing Computer Use Any App/native app controls.", + linuxStatus: "existing-linux-substrate", + }; + } + + if (lower.includes("computer-use-native-desktop-app-icon")) { + return { + category: "linux-parity-drift", + confidence: "high", + confidenceRationale: "Exact Computer Use native icon contract matched.", + feature: "Computer Use native app icons", + issueCandidate: false, + linuxSurfaceId: "computer_use_plugin", + patchTarget: "scripts/patches/impl/computer-use.js", + recommendation: "Patch the native desktop app icon query gate so Linux can request native app icons for Computer Use cards and mentions.", + linuxStatus: "existing-linux-substrate", + }; + } + + if (isComputerUsePlatformGateContext(lower, pathLower)) { + return { + category: "linux-parity-drift", + confidence: "high", + confidenceRationale: "Exact Computer Use renderer contract matched.", + feature: "Computer Use native app UI", + issueCandidate: false, + linuxSurfaceId: "computer_use_plugin", + patchTarget: "scripts/patches/impl/computer-use.js", + recommendation: "Patch the renderer availability gate so Linux exposes the existing Computer Use backend in settings and @ mentions.", + linuxStatus: "existing-linux-substrate", + }; + } + + if (pathLower.includes("computer-use") || lower.includes("computer use") || lower.includes("computer-use")) { + return { + category: "needs-review", + confidence: "medium", + confidenceRationale: "Computer Use mention is present without an exact Linux parity contract.", + feature: "Computer Use generic platform mention", + issueCandidate: false, + linuxSurfaceId: "computer_use_plugin", + patchTarget: "scripts/patches/impl/computer-use.js", + linuxStatus: "existing-linux-substrate", + recommendation: "Review the current-bundle context before treating this generic mention as a Linux parity blocker.", + }; + } + + if (lower.includes("microsoftexcel") || lower.includes("microsoftpowerpoint")) { + return { + category: "platform-specific-unsupported", + confidence: "high", + confidenceRationale: "Exact Office native-app marker matched.", + feature: "Office live-control app mentions", + issueCandidate: true, + linuxSurfaceId: null, + patchTarget: "new issue or optional linux-features//", + recommendation: "Label as macOS/Windows-only until a Linux native app bridge exists; create a feature issue if parity is desired.", + linuxStatus: "unsupported-no-substrate", + }; + } + + if ( + lower.includes("settogglehotkey") || + lower.includes("synccommandkeybindings") || + lower.includes("commandkeybindings") || + lower.includes("globalhotkey") + ) { + return { + category: "platform-specific-unsupported", + confidence: "high", + confidenceRationale: "Exact global-hotkey marker matched.", + feature: "Global hotkey/keybinding integration", + issueCandidate: true, + linuxSurfaceId: null, + patchTarget: "new issue or optional linux-features//", + recommendation: "Label as macOS/Windows-only until a Linux global shortcut backend exists; create a feature issue if parity is desired.", + linuxStatus: "unsupported-no-substrate", + }; + } + + if (lower.includes("agi intelligence") || lower.includes("supreme")) { + return { + category: "new-upstream-capability", + confidence: "high", + feature: "Unmapped high-signal desktop capability", + issueCandidate: true, + linuxSurfaceId: null, + patchTarget: "scripts/dev/upstream-dmg-protected-surfaces.json plus a new Linux feature or backend owner", + recommendation: "Create an issue for the new upstream desktop capability and decide whether Linux needs a native port.", + }; + } + + if (lower.includes("chronicle") || lower.includes("skysight") || lower.includes("recording") || lower.includes("event_stream")) { + return { + category: "needs-review", + confidence: "medium", + feature: "Chronicle/Skysight/Record & Replay desktop capability", + issueCandidate: true, + linuxSurfaceId: lower.includes("chronicle") ? "chronicle_settings_toggles" : "record_and_replay_plugin", + patchTarget: "linux-features/record-and-replay and record-replay-linux", + recommendation: "Review whether this gate hides an existing Linux-backed feature or a new upstream desktop capability.", + confidenceRationale: "Generic Chronicle/Skysight text without an exact contract.", + }; + } + + if ( + lower.includes("titlebar") || + lower.includes("trafficlight") || + lower.includes("dock") || + lower.includes("tray") || + lower.includes("windowbutton") || + pathLower.includes("titlebar") + ) { + return { + category: "expected-platform-native", + confidence: "medium", + feature: "OS-native window chrome or shell integration", + issueCandidate: false, + linuxSurfaceId: null, + patchTarget: "none by default", + recommendation: "Keep labeled as platform-native unless the Linux window shell regresses.", + }; + } + + if ( + lower.includes("native") || + lower.includes("desktop") || + lower.includes("appplugin") || + lower.includes("sidecar") || + lower.includes("mcp") || + lower.includes("plugin") + ) { + return { + category: "new-upstream-capability", + confidence: "medium", + feature: "Unmapped desktop/native/plugin capability", + issueCandidate: true, + linuxSurfaceId: null, + patchTarget: "scripts/dev/upstream-dmg-protected-surfaces.json plus a new Linux feature or backend owner", + recommendation: "Create an issue, decide whether Linux needs a port or explicit unsupported label, and add a protected surface if accepted.", + }; + } + + return { + category: "needs-review", + confidence: "low", + feature: "Unclassified platform gate", + issueCandidate: false, + linuxSurfaceId: null, + patchTarget: "manual review", + recommendation: "Classify this gate as Linux parity, new capability, unsupported, or expected platform-native before accepting release drift.", + }; +} + +function createPlatformGateEntry({ file, gate, index, patternName }) { + const context = platformGateContext(file, index, gate.length); + const classification = classifyPlatformGate({ file, gate, context }); + return { + id: sha256(Buffer.from(`${file.relativePath}\0${gate}\0${compactSnippet(context)}`)).slice(0, 16), + path: file.relativePath, + gate, + platforms: gate.includes("darwin") || gate.includes("win32") ? ["darwin", "win32"] : ["macOS", "windows"], + pattern: patternName, + snippet: compactSnippet(context), + evidenceType: "platform-gate-context", + platformLabel: classification.platformLabel ?? (gate.includes("darwin") || gate.includes("win32") ? "macOS/windows" : "unknown"), + entitlement: classification.entitlement ?? "unknown", + rollout: classification.rollout ?? "unknown", + linuxStatus: classification.linuxStatus ?? "unknown", + ownerPath: classification.patchTarget ?? "manual review", + recommendedAction: classification.recommendation, + ...classification, + }; +} + +function createPlatformGateMap({ inventory, patchFindings = [] } = {}) { + const gates = []; + const seen = new Set(); + const gatePatterns = [ + { + name: "ui-platform-macos-windows", + regex: /[A-Za-z_$][\w$]*===`macOS`\|\|[A-Za-z_$][\w$]*===`windows`(?:\|\|[A-Za-z_$][\w$]*===`linux`)?/g, + }, + { + name: "process-platform-darwin-win32", + regex: /process\.platform===`darwin`\|\|process\.platform===`win32`|process\.platform!==`linux`&&process\.platform!==`darwin`/g, + }, + ]; + + for (const file of inventory.files ?? []) { + if (file.text == null) { + continue; + } + for (const pattern of gatePatterns) { + for (const match of file.text.matchAll(pattern.regex)) { + const entry = createPlatformGateEntry({ + file, + gate: match[0], + index: match.index, + patternName: pattern.name, + }); + const key = `${entry.path}\0${entry.gate}\0${entry.category}\0${entry.feature}`; + if (!seen.has(key)) { + seen.add(key); + gates.push(entry); + } + } + } + + if ( + file.text.includes("computer-use-plugin-icon-linux.png") && + file.text.includes("availablePlugins.some") && + file.text.includes("plugin?.name") && + !file.text.includes("e.plugin?.installed===!0&&e.plugin?.enabled===!0") + ) { + const entry = { + id: sha256(Buffer.from(`${file.relativePath}\0computer-use-settings-mask`)).slice(0, 16), + path: file.relativePath, + gate: "synthetic Linux Computer Use row masked by unavailable upstream plugin", + platforms: ["linux"], + pattern: "synthetic-plugin-mask", + snippet: textSnippet(file.text, "computer-use-plugin-icon-linux.png"), + category: "linux-parity-drift", + confidence: "high", + feature: "Computer Use settings row", + issueCandidate: false, + linuxSurfaceId: "computer_use_plugin", + patchTarget: "scripts/patches/impl/computer-use.js", + recommendation: "Prepend the enabled synthetic Linux Computer Use plugin unless an installed and enabled upstream entry already exists.", + }; + const key = `${entry.path}\0${entry.gate}`; + if (!seen.has(key)) { + seen.add(key); + gates.push(entry); + } + } + } + + const findingsByName = new Map(patchFindings.map((finding) => [finding.name, finding])); + for (const gate of gates) { + if (gate.category !== "linux-parity-drift") { + continue; + } + const requiredPatches = LINUX_PARITY_SURFACE_PATCHES[gate.linuxSurfaceId] ?? []; + if (requiredPatches.length === 0) { + continue; + } + const coverage = requiredPatches.map((name) => findingsByName.get(name) ?? { name, status: "not-recorded" }); + gate.patchPreflight = coverage.map(({ name, status }) => ({ name, status })); + if (coverage.every((finding) => ["applied", "already-applied"].includes(finding.status))) { + gate.rawCategory = gate.category; + gate.category = "patched-linux-parity"; + gate.linuxStatus = "patched-for-candidate"; + gate.recommendedAction = "No action; every exact Linux parity patch applied to this candidate."; + gate.recommendation = gate.recommendedAction; + } + } + + const categoryCounts = {}; + for (const gate of gates) { + categoryCounts[gate.category] = (categoryCounts[gate.category] ?? 0) + 1; + } + + return { + generatedAt: new Date().toISOString(), + source: inventory.source, + categoryCounts, + blockingCount: gates.filter((gate) => PLATFORM_GATE_BLOCKING_CATEGORIES.has(gate.category)).length, + reviewCount: gates.filter((gate) => PLATFORM_GATE_REVIEW_CATEGORIES.has(gate.category)).length, + gates: gates.sort((a, b) => `${a.category}:${a.feature}:${a.path}`.localeCompare(`${b.category}:${b.feature}:${b.path}`)), + }; +} + function classifySurfaceDrift({ baselineSurface, candidateSurface }) { const baselinePresent = baselineSurface?.status === "PRESENT"; const candidatePresent = candidateSurface?.status === "PRESENT"; @@ -1476,6 +2280,176 @@ function summarizeMapDrift(mapDrift) { return summary; } +function capabilityId(parts) { + return sha256(Buffer.from(parts.filter(Boolean).join("\0"))).slice(0, 16); +} + +function capabilityFromPlatformGate(gate) { + if (!["new-upstream-capability", "platform-specific-unsupported"].includes(gate.category)) { + return null; + } + return { + id: `platform-gate:${gate.id}`, + type: "platform-gate", + name: gate.feature, + path: gate.path, + category: gate.category, + confidence: gate.confidence, + issueCandidate: gate.issueCandidate, + recommendation: gate.recommendation, + patchTarget: gate.patchTarget, + evidence: gate.gate, + platformLabel: gate.platformLabel, + entitlement: gate.entitlement, + rollout: gate.rollout, + evidenceType: gate.evidenceType, + confidenceRationale: gate.confidenceRationale ?? "Classifier default; manual review required.", + linuxStatus: gate.linuxStatus, + ownerPath: gate.ownerPath, + recommendedAction: gate.recommendation, + }; +} + +function nativeBinaryIsFeatureCandidate(binaryPath) { + const normalized = normalizePath(binaryPath); + return !( + normalized.startsWith("Contents/Frameworks/") || + normalized.startsWith("Contents/MacOS/") || + normalized.startsWith("Contents/Resources/cua_node/") || + normalized.includes("/plugins/") || + normalized.includes("/node_modules/") + ); +} + +function createNewCapabilityMap({ mapDrift, platformGateMap, candidatePluginMap } = {}) { + const capabilities = []; + const addCapability = (capability) => { + if (capability == null) { + return; + } + capabilities.push({ + confidence: "medium", + issueCandidate: true, + patchTarget: "scripts/dev/upstream-dmg-protected-surfaces.json", + recommendation: "Create an issue, classify Linux support, and add a protected surface if this capability needs parity.", + platformLabel: "unknown", + entitlement: "unknown", + rollout: "unknown", + evidenceType: "bundle-drift", + confidenceRationale: "Capability discovered from current-bundle structural drift.", + linuxStatus: "unknown", + ownerPath: "scripts/dev/upstream-dmg-protected-surfaces.json", + recommendedAction: "Create an issue and classify Linux support.", + bundlePresent: true, + uiExposed: null, + serverGateObserved: false, + entitlementProven: false, + ...capability, + }); + }; + + if (mapDrift?.mode === "baselineComparison") { + for (const pluginId of mapDrift?.pluginDrift?.added ?? []) { + addCapability({ + id: `plugin:${pluginId}`, + type: "plugin", + name: pluginId, + category: "new-upstream-capability", + evidence: pluginId, + }); + } + + for (const key of mapDrift?.mcpDrift?.added ?? []) { + const [pluginId, serverName, toolName] = key.split(":"); + addCapability({ + id: `mcp:${key}`, + type: "mcp-tool", + name: toolName ?? serverName ?? key, + category: "new-upstream-capability", + evidence: key, + recommendation: `Review new MCP tool ${key}; add Linux backend coverage or an unsupported label.`, + patchTarget: pluginId ? `plugins/openai-bundled/plugins/${pluginId} or matching linux-features owner` : "matching Linux MCP owner", + }); + } + + for (const binaryPath of mapDrift?.nativeBinaryDrift?.added ?? []) { + if (!nativeBinaryIsFeatureCandidate(binaryPath)) continue; + addCapability({ + id: `native:${binaryPath}`, + type: "native-binary", + name: path.posix.basename(binaryPath), + path: binaryPath, + category: "new-upstream-capability", + evidence: binaryPath, + recommendation: "Review the new native binary for Linux replacement, staging, or explicit unsupported status.", + patchTarget: "scripts/lib/bundled-plugins.sh or a dedicated linux-features// owner", + }); + } + + for (const key of mapDrift?.bridgeHandlerDrift?.added ?? []) { + addCapability({ + id: `bridge:${capabilityId(["bridge", key])}`, + type: "bridge-handler", + name: key.split(":")[1] ?? key, + category: "new-upstream-capability", + evidence: key, + recommendation: "Review the new Electron bridge handler and decide whether Linux needs a native mirror or patch.", + patchTarget: "scripts/patches/core/all-linux or matching linux-features owner", + }); + } + } + + const sites = (candidatePluginMap?.plugins ?? []).find((plugin) => plugin.id === "sites"); + if (sites != null && (mapDrift?.pluginDrift?.added ?? []).includes("sites")) { + const manifest = sites.manifests?.[0] ?? {}; + addCapability({ + id: "plugin:sites", + type: "plugin", + name: "Sites", + version: manifest.version ?? "unknown", + category: "cross-platform-entitlement-gated", + platformLabel: "cross-platform", + entitlement: "connector_20205bf7d4e99a89d7154bb849718324", + entitlementLabel: "connector_20205bf7d4e99a89d7154bb849718324 (server entitlement not proven)", + rollout: "unknown", + evidenceType: "plugin-manifest-and-app-registration", + confidence: "high", + confidenceRationale: "Current bundle contains the Sites manifest and connector registration; server entitlement is not proven.", + bundlePresent: true, + uiExposed: Boolean(manifest.displayName), + serverGateObserved: false, + entitlementProven: false, + linuxStatus: "staging-ownership-needed", + patchTarget: "scripts/lib/bundled-plugins.sh", + ownerPath: "scripts/lib/bundled-plugins.sh", + recommendation: "Create a staging issue for Sites and keep entitlement unknown until server evidence is available.", + recommendedAction: "Create a staging issue; do not infer entitlement from bundle presence.", + evidence: sites.files, + }); + } + + for (const gate of platformGateMap?.gates ?? []) { + addCapability(capabilityFromPlatformGate(gate)); + } + + const deduped = new Map(); + for (const capability of capabilities) { + deduped.set(capability.id, capability); + } + const items = [...deduped.values()].sort((a, b) => `${a.category}:${a.type}:${a.name}`.localeCompare(`${b.category}:${b.type}:${b.name}`)); + const categoryCounts = {}; + for (const item of items) { + categoryCounts[item.category] = (categoryCounts[item.category] ?? 0) + 1; + } + return { + generatedAt: new Date().toISOString(), + mode: mapDrift?.mode ?? "unknown", + categoryCounts, + issueCandidateCount: items.filter((item) => item.issueCandidate).length, + capabilities: items, + }; +} + function markdownList(items) { if (items.length === 0) { return "- None\n"; @@ -1483,6 +2457,51 @@ function markdownList(items) { return items.map((item) => `- ${item}`).join("\n") + "\n"; } +function markdownCell(value) { + const normalized = value != null && typeof value === "object" ? JSON.stringify(value) : String(value ?? ""); + return normalized + .replace(/\s+/g, " ") + .replace(/\|/g, "\\|") + .trim(); +} + +function markdownTable(headers, rows) { + if (rows.length === 0) { + return "None.\n"; + } + const headerLine = `| ${headers.map(markdownCell).join(" | ")} |`; + const dividerLine = `| ${headers.map(() => "---").join(" | ")} |`; + const rowLines = rows.map((row) => `| ${row.map(markdownCell).join(" | ")} |`); + return `${[headerLine, dividerLine, ...rowLines].join("\n")}\n`; +} + +function platformGateRows(gates, categories, limit = PLATFORM_GATE_MARKDOWN_LIMIT) { + return (gates ?? []) + .filter((gate) => categories.includes(gate.category)) + .slice(0, limit) + .map((gate) => [ + gate.feature, + gate.category, + gate.path, + gate.patchTarget, + gate.recommendation, + ]); +} + +function capabilityRows(capabilities, limit = NEW_CAPABILITY_MARKDOWN_LIMIT) { + return (capabilities ?? []) + .slice(0, limit) + .map((capability) => [ + capability.name, + capability.version ?? "", + capability.type, + capability.category, + `${capability.path ?? capability.evidence ?? ""}${capability.entitlementLabel ? `; ${capability.entitlementLabel}` : capability.entitlement ? `; entitlement=${capability.entitlement}` : ""}`, + capability.patchTarget, + capability.recommendation, + ]); +} + function renderDriftMarkdown(report) { const lines = ["# Upstream DMG Drift Report", ""]; lines.push(`Generated: ${report.generatedAt}`); @@ -1491,6 +2510,64 @@ function renderDriftMarkdown(report) { lines.push(`Baseline: ${report.baselineSource.path}`); } lines.push(""); + lines.push("## App and bundle versions"); + lines.push(""); + lines.push(markdownTable( + ["Field", "Baseline", "Candidate", "Evidence"], + Object.keys(report.versionDelta ?? {}).map((field) => [ + field, + report.versionDelta[field]?.baseline ?? "unknown", + report.versionDelta[field]?.candidate ?? "unknown", + report.versionDelta[field]?.evidence ?? "unknown", + ]), + ).trimEnd()); + lines.push(""); + lines.push("## Feature staging and runtime health"); + lines.push(""); + lines.push(markdownTable( + ["Feature", "Owner", "Staging", "Runtime status", "Action"], + (report.featureStaging?.features ?? []).map((feature) => [ + feature.id, + feature.targetOwnership, + feature.stagingPresent ? "present" : "missing", + feature.id === "mcp-helper-reaper" ? (report.runtimeHealth?.mcpHelperReaper?.status ?? "unknown") : "not-run", + feature.id === "mcp-helper-reaper" ? "Supply a runtime snapshot before claiming health." : "Compare current-bundle staging against this owner.", + ]), + ).trimEnd()); + lines.push(""); + lines.push("## Linux Parity Drift"); + lines.push(""); + lines.push(markdownTable( + ["Feature", "Category", "Path", "Patch target", "Recommendation"], + platformGateRows(report.platformGates, ["linux-parity-drift"]), + ).trimEnd()); + lines.push(""); + lines.push("## Required patch preflight"); + lines.push(""); + lines.push(markdownTable( + ["Patch", "Status", "Severity", "Owner", "Recommendation"], + (report.requiredPatchPreflight?.findings ?? []).map((finding) => [finding.name, finding.status, finding.severity, finding.ownerPath, finding.recommendation]), + ).trimEnd()); + lines.push(""); + lines.push("## New Capability Candidates"); + lines.push(""); + lines.push(markdownTable( + ["Name", "Version", "Type", "Category", "Evidence", "Owner", "Recommendation"], + capabilityRows(report.newCapabilities), + ).trimEnd()); + lines.push(""); + lines.push("## Platform-Specific Labels"); + lines.push(""); + lines.push(markdownTable( + ["Feature", "Category", "Path", "Patch target", "Recommendation"], + platformGateRows(report.platformGates, [ + "platform-specific-unsupported", + "expected-platform-native", + "needs-review", + "already-linux-enabled", + ]), + ).trimEnd()); + lines.push(""); lines.push("## Classification Counts"); lines.push(""); lines.push( @@ -1583,6 +2660,10 @@ function changedPayloadList(entries, maxItems = ACTION_PLAN_PATH_SAMPLE_LIMIT) { function renderActionPlanMarkdown(driftReport, candidateProtected, mapDrift = null) { const actionable = driftReport.surfaceDrift.filter((item) => ACTIONABLE_CLASSIFICATIONS.has(item.classification)); + const platformBlockers = (driftReport.platformGates ?? []).filter((gate) => + PLATFORM_GATE_BLOCKING_CATEGORIES.has(gate.category), + ); + const capabilityCandidates = driftReport.newCapabilities ?? []; const structuralSummary = driftReport.structuralDriftSummary ?? summarizeMapDrift(mapDrift); const lines = ["# Linux Substrate Action Plan", ""]; lines.push(`Candidate: ${candidateProtected.source?.path ?? "unknown"}`); @@ -1591,10 +2672,30 @@ function renderActionPlanMarkdown(driftReport, candidateProtected, mapDrift = nu lines.push(`Structural maps: ${summaryLine}`); } lines.push(""); - if (actionable.length === 0) { + if (actionable.length === 0 && platformBlockers.length === 0 && capabilityCandidates.length === 0) { lines.push("No protected-surface action required by this report."); return `${lines.join("\n")}\n`; } + if (platformBlockers.length > 0) { + lines.push("## Linux parity blockers"); + for (const gate of platformBlockers) { + lines.push(`- ${gate.feature}: ${gate.recommendation}`); + lines.push(` Patch target: ${gate.patchTarget}`); + lines.push(` Evidence: ${gate.path} :: ${gate.gate}`); + } + lines.push(""); + } + if (capabilityCandidates.length > 0) { + lines.push("## New capability / issue candidates"); + for (const capability of capabilityCandidates.slice(0, NEW_CAPABILITY_MARKDOWN_LIMIT)) { + lines.push(`- ${capability.name} (${capability.category}): ${capability.recommendation}`); + lines.push(` Owner: ${capability.patchTarget}`); + } + if (capabilityCandidates.length > NEW_CAPABILITY_MARKDOWN_LIMIT) { + lines.push(`- ${capabilityCandidates.length - NEW_CAPABILITY_MARKDOWN_LIMIT} additional candidates omitted; see new-capabilities.json.`); + } + lines.push(""); + } for (const item of actionable) { lines.push(`## ${item.surfaceId}`); lines.push(`Classification: ${item.classification}`); @@ -1657,6 +2758,31 @@ function publicInventory(inventory) { }; } +function createFeatureStagingInventory(repoRoot) { + const root = path.join(repoRoot, "linux-features"); + const features = []; + if (!fs.existsSync(root)) return { generatedAt: new Date().toISOString(), features }; + for (const id of fs.readdirSync(root).sort()) { + const featureRoot = path.join(root, id); + if (!fs.statSync(featureRoot).isDirectory()) continue; + const featureJson = path.join(featureRoot, "feature.json"); + let manifest = null; + if (fs.existsSync(featureJson)) { + try { manifest = readJson(featureJson); } catch { manifest = null; } + } + const files = fs.readdirSync(featureRoot).filter((name) => !name.startsWith(".")); + features.push({ + id, + manifestPath: fs.existsSync(featureJson) ? `linux-features/${id}/feature.json` : null, + entrypoints: manifest?.entrypoints ?? {}, + sourcePaths: files.map((name) => `linux-features/${id}/${name}`), + targetOwnership: manifest?.targetOwnership ?? `linux-features/${id}`, + stagingPresent: files.some((name) => /stage|patch|plugin|cleanup/i.test(name)), + }); + } + return { generatedAt: new Date().toISOString(), features }; +} + function sameResolvedPath(left, right) { try { return fs.realpathSync(left) === fs.realpathSync(right); @@ -1679,6 +2805,22 @@ function resolveBaselinePath({ autoBaseline = false, baselinePath = null, candid return defaultBaselinePath; } +function mergeProvenance(detected = {}, supplied = null) { + if (supplied == null) return detected; + const merged = { ...detected, ...supplied }; + for (const key of ["candidate", "baseline"]) { + const detectedEntry = detected?.[key] ?? null; + const suppliedEntry = supplied?.[key] ?? null; + merged[key] = + detectedEntry == null + ? suppliedEntry + : suppliedEntry == null + ? detectedEntry + : { ...detectedEntry, ...suppliedEntry }; + } + return merged; +} + function buildIntelReports({ autoBaseline = false, baselinePath = null, @@ -1688,6 +2830,8 @@ function buildIntelReports({ registry, repoRoot = process.cwd(), timestamp = null, + provenance = null, + runPatchPreflight = false, } = {}) { if (candidatePath == null) { throw new Error("candidatePath is required"); @@ -1731,20 +2875,108 @@ function buildIntelReports({ }); const patchReport = patchReportPath != null && fs.existsSync(patchReportPath) ? readJson(patchReportPath) : null; + const requiredPatchPreflight = runPatchPreflight + ? runRequiredPatchPreflight({ inventory: candidateInventory, repoRoot, workDir: scratchRoot }) + : { status: "not-run", findings: [] }; + const effectivePatchReport = patchReport == null && requiredPatchPreflight.status === "not-run" + ? null + : { + ...(patchReport ?? { patches: [] }), + patches: [...(patchReport?.patches ?? []), ...requiredPatchPreflight.findings.map((finding) => ({ + name: finding.name, + status: finding.severity === "none" ? "already-applied" : "failed-required", + ciPolicy: "required-upstream", + reason: finding.reason ?? finding.recommendation, + }))], + }; const driftReport = compareProtectedSurfaces({ baseline: baselineProtected, candidate: candidateProtected, - patchReport, + patchReport: effectivePatchReport, }); + const baselineVersions = baselineInventory?.versionMetadata ?? {}; + const candidateVersions = candidateInventory.versionMetadata ?? {}; + const versionDelta = {}; + const versionFields = [ + "cfBundleShortVersionString", + "cfBundleVersion", + "appPackageVersion", + "electronVersion", + "codexCliVersion", + ]; + for (const field of versionFields) { + versionDelta[field] = { + baseline: baselineVersions[field] ?? null, + candidate: candidateVersions[field] ?? null, + changed: (baselineVersions[field] ?? null) !== (candidateVersions[field] ?? null), + evidence: [...(baselineVersions.evidence ?? []), ...(candidateVersions.evidence ?? [])] + .filter((entry) => entry.field === field).map((entry) => entry.path).join(", ") || null, + }; + } + versionDelta.bundledPluginVersions = { + baseline: baselineVersions.bundledPluginVersions ?? {}, + candidate: candidateVersions.bundledPluginVersions ?? {}, + changed: JSON.stringify(baselineVersions.bundledPluginVersions ?? {}) !== JSON.stringify(candidateVersions.bundledPluginVersions ?? {}), + evidence: "plugin manifests", + }; + driftReport.versionDelta = versionDelta; + const detectedProvenance = { + candidate: candidatePath.endsWith(".dmg") && fs.statSync(candidatePath).isFile() ? { + url: process.env.CODEX_UPSTREAM_DMG_URL ?? null, + bytes: fs.statSync(candidatePath).size, + sha256: sha256File(candidatePath), + etag: process.env.CODEX_UPSTREAM_DMG_ETAG ?? null, + lastModified: process.env.CODEX_UPSTREAM_DMG_LAST_MODIFIED ?? null, + } : null, + baseline: resolvedBaselinePath?.endsWith(".dmg") && fs.statSync(resolvedBaselinePath).isFile() ? { + url: null, + bytes: fs.statSync(resolvedBaselinePath).size, + sha256: sha256File(resolvedBaselinePath), + etag: null, + lastModified: null, + } : null, + }; + driftReport.provenance = mergeProvenance(detectedProvenance, provenance); + const featureStaging = createFeatureStagingInventory(repoRoot); + driftReport.featureStaging = featureStaging; + driftReport.runtimeHealth = { + mcpHelperReaper: { + status: "UNKNOWN", + evidenceType: "static-source-only", + message: "No runtime snapshot supplied; helper liveness, ownership, duplicate state, and cleanup were not run.", + ownerPath: "linux-features/mcp-helper-reaper/reaper/src/lib.rs", + }, + }; + driftReport.requiredPatchPreflight = requiredPatchPreflight; const mapDrift = compareMaps({ baselineProtected, candidateProtected }); + const platformGateMap = createPlatformGateMap({ + inventory: candidateInventory, + patchFindings: requiredPatchPreflight.findings, + }); + const newCapabilityMap = createNewCapabilityMap({ mapDrift, platformGateMap, candidatePluginMap: candidateProtected.pluginMap }); driftReport.structuralDriftSummary = summarizeMapDrift(mapDrift); + driftReport.platformGateSummary = { + categoryCounts: platformGateMap.categoryCounts, + blockingCount: platformGateMap.blockingCount, + reviewCount: platformGateMap.reviewCount, + }; + driftReport.platformGates = platformGateMap.gates.slice(0, 50); + driftReport.newCapabilitySummary = { + categoryCounts: newCapabilityMap.categoryCounts, + issueCandidateCount: newCapabilityMap.issueCandidateCount, + }; + driftReport.newCapabilities = newCapabilityMap.capabilities.slice(0, 50); fs.mkdirSync(reportDir, { recursive: true }); writeJson(path.join(reportDir, "inventory.json"), publicInventory(candidateInventory)); + writeJson(path.join(reportDir, "feature-staging.json"), featureStaging); + writeJson(path.join(reportDir, "required-patch-preflight.json"), requiredPatchPreflight); writeJson(path.join(reportDir, "protected-surfaces.json"), candidateProtected); writeJson(path.join(reportDir, "bridge-map.json"), candidateProtected.bridgeMap); writeJson(path.join(reportDir, "plugin-map.json"), candidateProtected.pluginMap); writeJson(path.join(reportDir, "native-binary-map.json"), candidateProtected.nativeBinaryMap); + writeJson(path.join(reportDir, "platform-gates.json"), platformGateMap); + writeJson(path.join(reportDir, "new-capabilities.json"), newCapabilityMap); writeJson(path.join(reportDir, "map-drift.json"), mapDrift); writeJson(path.join(reportDir, "drift-report.json"), driftReport); fs.writeFileSync(path.join(reportDir, "drift-report.md"), renderDriftMarkdown(driftReport), "utf8"); @@ -1773,6 +3005,8 @@ function buildIntelReports({ protectedSurfaces: candidateProtected, driftReport, mapDrift, + platformGateMap, + newCapabilityMap, }; } finally { fs.rmSync(scratchRoot, { force: true, recursive: true }); @@ -1784,11 +3018,17 @@ module.exports = { compareProtectedSurfaces, createBridgeMap, createInventory, + runRequiredPatchPreflight, + extractVersionMetadata, + createNewCapabilityMap, createNativeBinaryMap, + createPlatformGateMap, createPluginMap, compareMaps, extractProtectedSurfaces, findPostPatchIntegrityFindings, + mergeProvenance, + prepareRequiredPatchPreflightApp, renderActionPlanMarkdown, renderDriftMarkdown, resolveBaselinePath,