From 1e899b5dc088e3cbfdbb9f0854696f27591e6731 Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Sun, 27 Sep 2026 08:18:15 +0800 Subject: [PATCH 1/6] chore: refresh the install-scripts allowlist against the lockfile npm's install-scripts approval gate flags seven packages on a fresh clone: two allowlist entries trail the lockfile (esbuild 0.27.7 vs the pinned 0.28.2, @jackwener/opencli 1.8.4 vs 1.8.7), and five packages with install scripts were never allowlisted at all (electron-winstaller, protobufjs, tree-sitter-javascript, @astryxdesign/cli, @astryxdesign/core). Every entry is now the exact version pinned in package-lock.json, verified against node_modules, and a fresh install completes with no unreviewed-script warnings. Generated-by: GLM-5.3-Flash (ZCode) --- package.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ba0b9adb45..5859af0329 100644 --- a/package.json +++ b/package.json @@ -127,9 +127,14 @@ "yaml": "2.9.1" }, "allowScripts": { - "esbuild@0.27.7": true, - "@jackwener/opencli@1.8.4": true, - "node-pty@1.2.0-beta.15": true + "@astryxdesign/cli@0.6.2": true, + "@astryxdesign/core@0.6.2": true, + "@jackwener/opencli@1.8.7": true, + "electron-winstaller@5.4.0": true, + "esbuild@0.28.2": true, + "node-pty@1.2.0-beta.15": true, + "protobufjs@7.6.5": true, + "tree-sitter-javascript@0.25.0": true }, "overrides": { "@ai-sdk/code-mode": { From 7bdbbe1e397b6b22d04048b9f9a96ea27653a95f Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Sun, 27 Sep 2026 13:14:55 +0800 Subject: [PATCH 2/6] chore: deny the four nonessential install scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: the @astryxdesign/cli and @astryxdesign/core postinstalls only print setup nudges, protobufjs only checks its version notation, and @jackwener/opencli skips its shell-completion postinstall on this repository's installs and in CI — none produces an artifact Maka needs. Record an explicit deny (which also clears npm's unreviewed-script warning without executing the scripts) and keep approval only for the scripts installation genuinely requires (esbuild's binary install, tree-sitter-javascript's native build, electron-winstaller's arch selection, node-pty's native build). Generated-by: GLM-5.3-Flash (ZCode) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5859af0329..58350cbe9a 100644 --- a/package.json +++ b/package.json @@ -127,13 +127,13 @@ "yaml": "2.9.1" }, "allowScripts": { - "@astryxdesign/cli@0.6.2": true, - "@astryxdesign/core@0.6.2": true, - "@jackwener/opencli@1.8.7": true, + "@astryxdesign/cli@0.6.2": false, + "@astryxdesign/core@0.6.2": false, + "@jackwener/opencli@1.8.7": false, "electron-winstaller@5.4.0": true, "esbuild@0.28.2": true, "node-pty@1.2.0-beta.15": true, - "protobufjs@7.6.5": true, + "protobufjs@7.6.5": false, "tree-sitter-javascript@0.25.0": true }, "overrides": { From aa31461d4c98442d3db2de8469c54d82c28bd2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E5=A4=A9=E8=B1=AA?= Date: Mon, 28 Sep 2026 10:23:00 +0800 Subject: [PATCH 3/6] chore: allowlist fsevents@2.3.3 install script fsevents@2.3.3 is a darwin-only optional dev dependency with hasInstallScript: true in the lockfile; a fresh install on macOS trips the approval gate exactly like the eight entries this PR already refreshed. The allowlist was scanned on Linux, where the os: ["darwin"] guard keeps the package out of node_modules - that is how it was missed. Generated-by: GLM-5.3-Flash (ZCode) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 58350cbe9a..80a9c39e2d 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@jackwener/opencli@1.8.7": false, "electron-winstaller@5.4.0": true, "esbuild@0.28.2": true, + "fsevents@2.3.3": true, "node-pty@1.2.0-beta.15": true, "protobufjs@7.6.5": false, "tree-sitter-javascript@0.25.0": true From c43d3b0774c10d032ceddaa9cf31be8003d1199f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E5=A4=A9=E8=B1=AA?= Date: Mon, 28 Sep 2026 11:17:23 +0800 Subject: [PATCH 4/6] chore(ci): guard allowScripts against lockfile drift npm's install-script approval gate keys approvals on exact name@version, so a routine bump silently stales the map and a new optional script- carrying dependency (fsevents) escapes it entirely. The new check-allow-scripts script fails on any script-carrying lockfile entry the map does not key, any map key trailing the lockfile, and any non-boolean decision; the dependency-audit workflow runs it beside the shipped-closure audit so the drift cannot land. Generated-by: GLM-5.3-Flash (ZCode) --- .github/workflows/dependency-audit.yml | 8 ++ scripts/check-allow-scripts.mjs | 137 +++++++++++++++++++++++++ scripts/check-allow-scripts.test.mjs | 123 ++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 scripts/check-allow-scripts.mjs create mode 100644 scripts/check-allow-scripts.test.mjs diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 4ce01265ff..99a6c6ea62 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -26,6 +26,7 @@ on: paths: - .github/workflows/dependency-audit.yml - scripts/audit-shipped-dependencies.mjs + - scripts/check-allow-scripts.mjs - scripts/third-party-closure.mjs - package.json - package-lock.json @@ -36,6 +37,7 @@ on: paths: - .github/workflows/dependency-audit.yml - scripts/audit-shipped-dependencies.mjs + - scripts/check-allow-scripts.mjs - scripts/third-party-closure.mjs - package.json - package-lock.json @@ -75,6 +77,12 @@ jobs: # not lock every pull request out of the repository. run: node scripts/audit-shipped-dependencies.mjs --allow-unavailable + - name: Check the allowScripts map against the lockfile + # npm's install-script approval gate keys on exact name@version; a + # routine bump or a new optional script-carrying dependency silently + # stales the map. Failing here keeps the drift from landing. + run: node scripts/check-allow-scripts.mjs + - name: Verify registry signatures # The full tree, not `--omit=dev`. Two reasons it has to be both: # the renderer roots live in devDependencies while their code ships diff --git a/scripts/check-allow-scripts.mjs b/scripts/check-allow-scripts.mjs new file mode 100644 index 0000000000..530f9bf3d2 --- /dev/null +++ b/scripts/check-allow-scripts.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Drift guard for the `allowScripts` map in the root package.json. +// +// npm >= 11.6 asks for an explicit approval before running a dependency's +// lifecycle scripts, keyed by exact `name@version`. The map only helps while +// it matches package-lock.json: this PR chain started because two keyed +// versions trailed the lockfile after routine bumps, and the optional +// Darwin-only fsevents was never listed at all. This check fails on any of: +// +// 1. a lockfile entry with `hasInstallScript` that the map does not key +// (optionality is not an exemption — fsevents is exactly the case); +// 2. a map key whose `name@version` no longer matches a lockfile entry +// that carries install scripts (the stale-version failure mode); +// 3. a map value that is not a boolean, which silently means neither +// "approve" nor "deny" to the gate. +// +// It does not judge the true/false decisions themselves — only that every +// script-carrying package has one, and that nothing in the map is dead. + +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const repoRoot = path.resolve( + path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')), + '..', +); +const args = process.argv.slice(2); + +function readOption(flag) { + const index = args.indexOf(flag); + return index === -1 ? null : args[index + 1]; +} + +const packageJsonPath = readOption('--package') ?? path.join(repoRoot, 'package.json'); +const lockfilePath = readOption('--lock') ?? path.join(repoRoot, 'package-lock.json'); + +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); +const lock = JSON.parse(readFileSync(lockfilePath, 'utf8')); + +const allowScripts = packageJson.allowScripts ?? null; +if (allowScripts === null || typeof allowScripts !== 'object' || Array.isArray(allowScripts)) { + console.error('package.json has no allowScripts object; nothing to guard.'); + process.exit(1); +} + +// A lockfile key is "node_modules/..." with the dependency name as the last +// `node_modules/` segment (scoped names span two path parts). +function nameFromLockKey(lockKey) { + const marker = lockKey.lastIndexOf('node_modules/'); + return marker === -1 ? lockKey : lockKey.slice(marker + 'node_modules/'.length); +} + +// Split an exact `name@version` key on its final '@' so scoped names survive. +function splitAllowKey(key) { + const at = key.lastIndexOf('@'); + if (at <= 0) return null; + return { name: key.slice(0, at), version: key.slice(at + 1) }; +} + +const problems = []; +const lockEntries = new Map(); // name@version -> lockfile key +for (const [lockKey, entry] of Object.entries(lock.packages ?? {})) { + if (lockKey === '') continue; // the root project is not a dependency of itself + if (!entry.hasInstallScript) continue; + const exact = `${nameFromLockKey(lockKey)}@${entry.version}`; + if (lockEntries.has(exact)) { + problems.push( + `lockfile carries ${exact} at two paths: ${lockEntries.get(exact)} and ${lockKey}`, + ); + continue; + } + lockEntries.set(exact, lockKey); +} + +for (const [exact] of lockEntries) { + if (!(exact in allowScripts)) { + const entry = lock.packages[lockEntries.get(exact)] ?? {}; + const where = entry.optional + ? ` (optional${entry.os?.length ? `, os: ${entry.os.join('/')}` : ''})` + : ''; + problems.push(`missing: ${exact}${where} has install scripts but no allowScripts decision`); + } +} + +const keyedExact = new Set(); +for (const [key, value] of Object.entries(allowScripts)) { + if (typeof value !== 'boolean') { + problems.push( + `invalid: "${key}" maps to ${JSON.stringify(value)}; the gate expects true or false`, + ); + continue; + } + const split = splitAllowKey(key); + if (!split || !split.version) { + problems.push(`invalid: "${key}" is not an exact name@version key`); + continue; + } + keyedExact.add(key); + if (!lockEntries.has(key)) { + problems.push( + `stale: "${key}" matches no lockfile entry with install scripts (version bumped, or scripts gone)`, + ); + } +} + +if (problems.length > 0) { + console.error(`allowScripts is out of sync with ${path.basename(lockfilePath)}:\n`); + for (const problem of problems) console.error(` - ${problem}`); + console.error( + `\n${keyedExact.size} keyed / ${lockEntries.size} script-carrying packages. Fix package.json's allowScripts map.`, + ); + process.exit(1); +} + +console.log( + `allowScripts covers all ${lockEntries.size} install-script packages with exact name@version keys.`, +); diff --git a/scripts/check-allow-scripts.test.mjs b/scripts/check-allow-scripts.test.mjs new file mode 100644 index 0000000000..3daae59c1a --- /dev/null +++ b/scripts/check-allow-scripts.test.mjs @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { execFile } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { test } from 'node:test'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const script = new URL('./check-allow-scripts.mjs', import.meta.url).pathname.replace( + /^\/([A-Za-z]:)/, + '$1', +); + +function writeFixture({ allowScripts, lockPackages }) { + const dir = mkdtempSync(path.join(tmpdir(), 'allow-scripts-')); + const packageJsonPath = path.join(dir, 'package.json'); + const lockfilePath = path.join(dir, 'package-lock.json'); + writeFileSync(packageJsonPath, JSON.stringify({ allowScripts })); + writeFileSync(lockfilePath, JSON.stringify({ packages: lockPackages })); + return { packageJsonPath, lockfilePath }; +} + +async function runCheck(fixture) { + try { + const { stdout } = await run(process.execPath, [ + script, + '--package', + fixture.packageJsonPath, + '--lock', + fixture.lockfilePath, + ]); + return { code: 0, stdout }; + } catch (error) { + return { code: error.code, stdout: '', stderr: String(error.stderr) }; + } +} + +const BASE_LOCK = { + '': { name: 'maka', version: '0.2.0', hasInstallScript: true }, + 'node_modules/esbuild': { version: '0.28.2', hasInstallScript: true }, + 'node_modules/fsevents': { + version: '2.3.3', + hasInstallScript: true, + optional: true, + os: ['darwin'], + }, + 'node_modules/node-pty': { version: '1.2.0-beta.15', hasInstallScript: true }, +}; + +test('a synced map passes', async () => { + const fixture = writeFixture({ + allowScripts: { + 'esbuild@0.28.2': true, + 'fsevents@2.3.3': true, + 'node-pty@1.2.0-beta.15': true, + }, + lockPackages: BASE_LOCK, + }); + const result = await runCheck(fixture); + if (result.code !== 0) throw new Error(`expected exit 0, got ${result.code}: ${result.stderr}`); +}); + +test('an unkeyed optional darwin-only package fails (the fsevents case)', async () => { + const fixture = writeFixture({ + allowScripts: { 'esbuild@0.28.2': true, 'node-pty@1.2.0-beta.15': true }, + lockPackages: BASE_LOCK, + }); + const result = await runCheck(fixture); + if (result.code !== 1) throw new Error(`expected exit 1, got ${result.code}`); + if (!result.stderr.includes('fsevents@2.3.3 (optional, os: darwin)')) + throw new Error(`missing fsevents note: ${result.stderr}`); +}); + +test('a map key trailing a lockfile bump fails (the stale-version case)', async () => { + const fixture = writeFixture({ + allowScripts: { + 'esbuild@0.27.7': true, + 'fsevents@2.3.3': true, + 'node-pty@1.2.0-beta.15': true, + }, + lockPackages: BASE_LOCK, + }); + const result = await runCheck(fixture); + if (result.code !== 1) throw new Error(`expected exit 1, got ${result.code}`); + if (!result.stderr.includes('stale: "esbuild@0.27.7"')) + throw new Error(`missing stale entry: ${result.stderr}`); + if (!result.stderr.includes('missing: esbuild@0.28.2')) + throw new Error(`missing missing-entry: ${result.stderr}`); +}); + +test('a non-boolean value fails', async () => { + const fixture = writeFixture({ + allowScripts: { + 'esbuild@0.28.2': 'yes', + 'fsevents@2.3.3': true, + 'node-pty@1.2.0-beta.15': true, + }, + lockPackages: BASE_LOCK, + }); + const result = await runCheck(fixture); + if (result.code !== 1) throw new Error(`expected exit 1, got ${result.code}`); + if (!result.stderr.includes('"esbuild@0.28.2" maps to "yes"')) + throw new Error(`missing invalid value: ${result.stderr}`); +}); From a5b07c507472ac04c5123440080a891896f955dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E5=A4=A9=E8=B1=AA?= Date: Mon, 28 Sep 2026 14:50:30 +0800 Subject: [PATCH 5/6] chore(ci): retrigger after a flaky code-mode run The affected-test lane failed on a single timing-sensitive assertion in code-mode.test.js ("excludes host waiting from the execution budget and preserves dependent Promise.race progress", false !== true). The same test file passes three consecutive local runs (33/33 each) on this exact tree; nothing in this PR touches the runtime. Tree-identical retrigger. Generated-by: GLM-5.3-Flash (ZCode) From 58d09e0a8825bb7e42df94d3f0bf972b714b965d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=B3=E5=A4=A9=E8=B1=AA?= Date: Mon, 28 Sep 2026 15:20:53 +0800 Subject: [PATCH 6/6] chore(ci): retrigger the windows packaging lane again The Release Windows package job crashed twice at the same point: the electron-builder icons@1.2.3 icon-tool child process died with 0xC0000005 on the Windows runner (the ico output had already been written). main's own package lane passes on the same sources, so this is runner-side, not this PR. Third attempt; if it crashes at the same point again I will file an issue with the three logs. Generated-by: GLM-5.3-Flash (ZCode)