From 664d8b7df8b9f0d09f0319d2ba95f19784fdf276 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 6 Oct 2026 21:06:47 +0800 Subject: [PATCH 1/4] ci: advance the Codewhale provider pin and regenerate verified bootstrap references --- .github/workflows/compatibility-upgrade.yml | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/compatibility-upgrade.yml diff --git a/.github/workflows/compatibility-upgrade.yml b/.github/workflows/compatibility-upgrade.yml new file mode 100644 index 0000000..fd9dd0c --- /dev/null +++ b/.github/workflows/compatibility-upgrade.yml @@ -0,0 +1,74 @@ +name: Validate installer upgrade +on: + push: + branches: [chore/plugin-compat-20261006] + paths: [.github/workflows/compatibility-upgrade.yml] +permissions: + contents: write +jobs: + upgrade: + if: github.ref == 'refs/heads/chore/plugin-compat-20261006' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + ref: chore/plugin-compat-20261006 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Create an immutable updated helper revision + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + p = Path('codewhale.mjs') + s = p.read_text() + assert "PROVIDER_REV = '8c78be0f936fb8f508badabc0195cdb21442a75d'" in s + s = s.replace('8c78be0f936fb8f508badabc0195cdb21442a75d', 'adad64b7ac77ec997b5be4662c6b6ccf36998357') + s = s.replace('api-key|config|config-path)', 'api-key|config|config-path|profile)') + s = s.replace('configuration or credentials', 'configuration, profile or credentials') + p.write_text(s) + PY + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add codewhale.mjs + git commit -m 'fix: update the Codewhale adapter and protect profile routing' + - name: Bind both bootstrap launchers to the exact helper bytes + run: | + python3 - <<'PY' + from pathlib import Path + import hashlib, subprocess + revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() + digest = hashlib.sha256(Path('codewhale.mjs').read_bytes()).hexdigest() + for name in ['codewhale.sh', 'codewhale.ps1', 'test-codewhale.mjs']: + p = Path(name) + s = p.read_text().replace('354a0e7e8597957b33f5d4f4afcf9ac7ebfe8693', revision) + s = s.replace('d985a055e9f5a95ad8b358d3569f4a9d33a1eaca52dffa39c4b8aa2268a3bbf1', digest) + if name == 'test-codewhale.mjs': + s = s.replace("'--base-url=x']", "'--base-url=x', '--profile', '--profile=personal']") + s += "\ntest('native prompt and continue short flags are not mistaken for route overrides', () => {\n assert.deepEqual(parseArguments(['run', '--', '-p', 'hello', '-c']).forwarded, ['-p', 'hello', '-c']);\n});\n" + p.write_text(s) + p = Path('CODEWHALE.md') + s = p.read_text().replace('8c78be0f936fb8f508badabc0195cdb21442a75d', 'adad64b7ac77ec997b5be4662c6b6ccf36998357') + s += '\n新版 Codewhale 的 `--profile` 不能透传覆盖 LMM 临时配置;原生 `-p`(提示词)和 `-c`(继续会话)保持可用。Bash 与 PowerShell 的独立下载入口固定到同一份助手提交,并校验相同 SHA-256。\n' + p.write_text(s) + print('Pinned helper revision:', revision) + print('Pinned helper SHA-256:', digest) + PY + - name: Validate installer contracts + run: | + set -euo pipefail + for script in *.sh; do bash -n "$script"; done + node --test test-codewhale.mjs test-opencode.mjs test-opencode-entry.mjs + python3 test-codewhale-menu.py + python3 test.py + python3 test-ai-tools.py + shellcheck *.sh + pwsh -NoProfile -File ./test-codewhale.ps1 + - name: Push only after the helper and both launchers validate together + run: | + set -euo pipefail + git add codewhale.sh codewhale.ps1 test-codewhale.mjs CODEWHALE.md + git commit -m 'fix: synchronize verified Codewhale bootstrap revisions and tests' + git push origin HEAD:refs/heads/chore/plugin-compat-20261006 From 319d9790ba026583ea226fb92b13b366b738c3ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:05 +0000 Subject: [PATCH 2/4] fix: update the Codewhale adapter and protect profile routing --- codewhale.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codewhale.mjs b/codewhale.mjs index 165f079..a581a46 100755 --- a/codewhale.mjs +++ b/codewhale.mjs @@ -7,7 +7,7 @@ import { createRequire } from 'node:module'; import { createInterface } from 'node:readline/promises'; import { pathToFileURL } from 'node:url'; -export const PROVIDER_REV = '8c78be0f936fb8f508badabc0195cdb21442a75d'; +export const PROVIDER_REV = 'adad64b7ac77ec997b5be4662c6b6ccf36998357'; export const PROVIDER_URL = `https://github.com/TokenNotIncluded/codewhale-lmm-provider/archive/${PROVIDER_REV}.tar.gz`; const require = createRequire(import.meta.url); const actions = ['setup', 'install', 'login', 'run', 'models', 'status', 'balance', 'usage', 'logout', 'doctor', 'menu', 'help']; @@ -49,8 +49,8 @@ export function parseArguments(argv) { if (!actions.includes(result.action)) throw new Error('Unknown action. Use --help.'); if (result.noBrowser && !['login', 'setup'].includes(result.action)) throw new Error('--no-browser is only valid with login/setup.'); if ((result.model || result.forwarded.length) && result.action !== 'run') throw new Error('--model and forwarded arguments are only valid with run.'); - if (result.forwarded.some(arg => /^--(?:provider|model|base-url|api-key|config|config-path)(?:=|$)/.test(arg))) { - throw new Error('Do not override the LMM provider, model, configuration or credentials after --.'); + if (result.forwarded.some(arg => /^--(?:provider|model|base-url|api-key|config|config-path|profile)(?:=|$)/.test(arg))) { + throw new Error('Do not override the LMM provider, model, configuration, profile or credentials after --.'); } return result; } From d211ef94861cbab681b9f31cf5c7fb489a5b2ed1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:29 +0000 Subject: [PATCH 3/4] fix: synchronize verified Codewhale bootstrap revisions and tests --- CODEWHALE.md | 4 +++- codewhale.ps1 | 4 ++-- codewhale.sh | 4 ++-- test-codewhale.mjs | 8 ++++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CODEWHALE.md b/CODEWHALE.md index b809124..9bef495 100644 --- a/CODEWHALE.md +++ b/CODEWHALE.md @@ -1,6 +1,6 @@ # Codewhale 一键安装和 LMM 配置 -需要 Node.js 22+ 和 npm。Bash 入口单独下载时还需要 curl。脚本不代装 Node,不改 npm registry,不使用 sudo 或 `--force`。安装使用官方的 `npm install --global codewhale@latest`;二进制选择和校验仍由官方 npm 安装器负责。LMM 适配器安装固定 Git 提交 `8c78be0f936fb8f508badabc0195cdb21442a75d`,禁用其 npm 生命周期脚本;不依赖尚未发布的 npm 包。 +需要 Node.js 22+ 和 npm。Bash 入口单独下载时还需要 curl。脚本不代装 Node,不改 npm registry,不使用 sudo 或 `--force`。安装使用官方的 `npm install --global codewhale@latest`;二进制选择和校验仍由官方 npm 安装器负责。LMM 适配器安装固定 Git 提交 `adad64b7ac77ec997b5be4662c6b6ccf36998357`,禁用其 npm 生命周期脚本;不依赖尚未发布的 npm 包。 ## 安装并配置 @@ -68,3 +68,5 @@ Node 测试使用模拟 npm、适配器和原生程序;菜单测试实际分 新增 CI 在 Ubuntu、macOS、Windows 运行离线测试,并使用隔离的 npm prefix 做真实下载安装和无账号启动检查;另覆盖 Windows PowerShell 5.1。CI 结果以对应提交为准。以上都不代替生产账号 OAuth、真实推理和 Termux 真机验收。 接口依据:[官方安装文档](https://github.com/Hmbown/Codewhale/blob/1554ac11846bc3e1423b0ee66591eea2870f10b1/docs/INSTALL.md)、[官方 npm 原生程序解析](https://github.com/Hmbown/Codewhale/blob/1554ac11846bc3e1423b0ee66591eea2870f10b1/npm/codewhale/scripts/run.js)、[LMM 适配器](https://github.com/TokenNotIncluded/codewhale-lmm-provider)。 + +新版 Codewhale 的 `--profile` 不能透传覆盖 LMM 临时配置;原生 `-p`(提示词)和 `-c`(继续会话)保持可用。Bash 与 PowerShell 的独立下载入口固定到同一份助手提交,并校验相同 SHA-256。 diff --git a/codewhale.ps1 b/codewhale.ps1 index 470d0f3..df13dc1 100644 --- a/codewhale.ps1 +++ b/codewhale.ps1 @@ -19,8 +19,8 @@ try { $work = Join-Path ([IO.Path]::GetTempPath()) ('lmm-codewhale-' + [guid]::NewGuid()) New-Item -ItemType Directory -Path $work | Out-Null $helper = Join-Path $work 'codewhale.mjs' - Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/354a0e7e8597957b33f5d4f4afcf9ac7ebfe8693/codewhale.mjs' -OutFile $helper - if ((Get-FileHash -LiteralPath $helper -Algorithm SHA256).Hash -ne 'd985a055e9f5a95ad8b358d3569f4a9d33a1eaca52dffa39c4b8aa2268a3bbf1') { + Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/319d9790ba026583ea226fb92b13b366b738c3ef/codewhale.mjs' -OutFile $helper + if ((Get-FileHash -LiteralPath $helper -Algorithm SHA256).Hash -ne '91dcff456f04abb0ef0ff7f95742ff3d332bfb2c93b4b067f4d778008556f69c') { throw 'Codewhale setup script checksum mismatch; nothing was executed.' } } diff --git a/codewhale.sh b/codewhale.sh index bac5ef3..e518b1d 100755 --- a/codewhale.sh +++ b/codewhale.sh @@ -10,6 +10,6 @@ temp_root=${TMPDIR:-/tmp} if [[ -n ${TERMUX_VERSION:-} || ${PREFIX:-} == */com.termux/files/usr ]]; then temp_root=${TMPDIR:-${PREFIX:-/data/data/com.termux/files/usr}/tmp}; fi work=$(mktemp -d "$temp_root/lmm-codewhale.XXXXXX") trap 'rm -rf -- "$work"' EXIT -curl --proto '=https' --proto-redir '=https' -fsSL 'https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/354a0e7e8597957b33f5d4f4afcf9ac7ebfe8693/codewhale.mjs' -o "$work/codewhale.mjs" -node -e 'const fs=require("node:fs"),crypto=require("node:crypto");if(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex")!==process.argv[2]){console.error("Codewhale setup script checksum mismatch; nothing was executed.");process.exit(1)}' "$work/codewhale.mjs" 'd985a055e9f5a95ad8b358d3569f4a9d33a1eaca52dffa39c4b8aa2268a3bbf1' +curl --proto '=https' --proto-redir '=https' -fsSL 'https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/319d9790ba026583ea226fb92b13b366b738c3ef/codewhale.mjs' -o "$work/codewhale.mjs" +node -e 'const fs=require("node:fs"),crypto=require("node:crypto");if(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex")!==process.argv[2]){console.error("Codewhale setup script checksum mismatch; nothing was executed.");process.exit(1)}' "$work/codewhale.mjs" '91dcff456f04abb0ef0ff7f95742ff3d332bfb2c93b4b067f4d778008556f69c' node "$work/codewhale.mjs" "$@" diff --git a/test-codewhale.mjs b/test-codewhale.mjs index c4b3ebf..b711293 100644 --- a/test-codewhale.mjs +++ b/test-codewhale.mjs @@ -65,7 +65,7 @@ test('unknown commands/options, missing values and cross-action flags fail close } }); test('provider, credential and config overrides cannot be forwarded', () => { - for (const flag of ['--api-key=secret', '--provider', '--config-path', '--model=other', '--base-url=x']) { + for (const flag of ['--api-key=secret', '--provider', '--config-path', '--model=other', '--base-url=x', '--profile', '--profile=personal']) { assert.throws(() => parseArguments(['run', '--', flag])); } }); @@ -240,7 +240,7 @@ process.exit(Number(process.env.TEST_CURL_EXIT||0));\n`; const result = f.runShell('--help'); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Codewhale \+ LMM/); - assert.ok(f.calls()[0][1].some(arg => /354a0e7e8597957b33f5d4f4afcf9ac7ebfe8693\/codewhale.mjs/.test(arg))); + assert.ok(f.calls()[0][1].some(arg => /319d9790ba026583ea226fb92b13b366b738c3ef\/codewhale.mjs/.test(arg))); }); test('truncated downloads fail before execution even when bytes were written', t => { const f = shellFixture(t); f.env.TEST_BAD_DOWNLOAD = '1'; f.env.TEST_CURL_EXIT = '18'; @@ -264,3 +264,7 @@ process.exit(Number(process.env.TEST_CURL_EXIT||0));\n`; assert.deepEqual(f.calls(), [['local', ['run', '--model', id, '--', 'exec', task]]]); }); } + +test('native prompt and continue short flags are not mistaken for route overrides', () => { + assert.deepEqual(parseArguments(['run', '--', '-p', 'hello', '-c']).forwarded, ['-p', 'hello', '-c']); +}); From 879e6bcaa566c45777c29c77e63fb5c9ad0ad620 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 6 Oct 2026 21:09:51 +0800 Subject: [PATCH 4/4] ci: remove one-time installer upgrade writer after successful validation --- .github/workflows/compatibility-upgrade.yml | 74 --------------------- 1 file changed, 74 deletions(-) delete mode 100644 .github/workflows/compatibility-upgrade.yml diff --git a/.github/workflows/compatibility-upgrade.yml b/.github/workflows/compatibility-upgrade.yml deleted file mode 100644 index fd9dd0c..0000000 --- a/.github/workflows/compatibility-upgrade.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Validate installer upgrade -on: - push: - branches: [chore/plugin-compat-20261006] - paths: [.github/workflows/compatibility-upgrade.yml] -permissions: - contents: write -jobs: - upgrade: - if: github.ref == 'refs/heads/chore/plugin-compat-20261006' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - with: - ref: chore/plugin-compat-20261006 - - uses: actions/setup-node@v4 - with: - node-version: '24' - - name: Create an immutable updated helper revision - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - p = Path('codewhale.mjs') - s = p.read_text() - assert "PROVIDER_REV = '8c78be0f936fb8f508badabc0195cdb21442a75d'" in s - s = s.replace('8c78be0f936fb8f508badabc0195cdb21442a75d', 'adad64b7ac77ec997b5be4662c6b6ccf36998357') - s = s.replace('api-key|config|config-path)', 'api-key|config|config-path|profile)') - s = s.replace('configuration or credentials', 'configuration, profile or credentials') - p.write_text(s) - PY - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add codewhale.mjs - git commit -m 'fix: update the Codewhale adapter and protect profile routing' - - name: Bind both bootstrap launchers to the exact helper bytes - run: | - python3 - <<'PY' - from pathlib import Path - import hashlib, subprocess - revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() - digest = hashlib.sha256(Path('codewhale.mjs').read_bytes()).hexdigest() - for name in ['codewhale.sh', 'codewhale.ps1', 'test-codewhale.mjs']: - p = Path(name) - s = p.read_text().replace('354a0e7e8597957b33f5d4f4afcf9ac7ebfe8693', revision) - s = s.replace('d985a055e9f5a95ad8b358d3569f4a9d33a1eaca52dffa39c4b8aa2268a3bbf1', digest) - if name == 'test-codewhale.mjs': - s = s.replace("'--base-url=x']", "'--base-url=x', '--profile', '--profile=personal']") - s += "\ntest('native prompt and continue short flags are not mistaken for route overrides', () => {\n assert.deepEqual(parseArguments(['run', '--', '-p', 'hello', '-c']).forwarded, ['-p', 'hello', '-c']);\n});\n" - p.write_text(s) - p = Path('CODEWHALE.md') - s = p.read_text().replace('8c78be0f936fb8f508badabc0195cdb21442a75d', 'adad64b7ac77ec997b5be4662c6b6ccf36998357') - s += '\n新版 Codewhale 的 `--profile` 不能透传覆盖 LMM 临时配置;原生 `-p`(提示词)和 `-c`(继续会话)保持可用。Bash 与 PowerShell 的独立下载入口固定到同一份助手提交,并校验相同 SHA-256。\n' - p.write_text(s) - print('Pinned helper revision:', revision) - print('Pinned helper SHA-256:', digest) - PY - - name: Validate installer contracts - run: | - set -euo pipefail - for script in *.sh; do bash -n "$script"; done - node --test test-codewhale.mjs test-opencode.mjs test-opencode-entry.mjs - python3 test-codewhale-menu.py - python3 test.py - python3 test-ai-tools.py - shellcheck *.sh - pwsh -NoProfile -File ./test-codewhale.ps1 - - name: Push only after the helper and both launchers validate together - run: | - set -euo pipefail - git add codewhale.sh codewhale.ps1 test-codewhale.mjs CODEWHALE.md - git commit -m 'fix: synchronize verified Codewhale bootstrap revisions and tests' - git push origin HEAD:refs/heads/chore/plugin-compat-20261006