chore(runtime): upgrade DSH and bundled plugins for runtime-7.10

This commit is contained in:
2026-10-26 11:22:36 +08:00
parent 9961d42a2f
commit 0389555d76
18 changed files with 118 additions and 50 deletions
+15 -1
View File
@@ -1,5 +1,19 @@
# DSH Runtime Release # DSH Runtime Release
## runtime-7.10:DSH rc.3 与内置插件更新(待运行验收)
沿用此前的 npm `next` 渠道,DSH 精确固定为 `0.2.1-rc.3`;查询时 `latest` 为 `0.2.1-rc.2`。mobile 更新为 `3.2.1`,free-search 为 `0.9.15`,Chat Manager 为 `1.5.18-dsh-mgr.1`(基于上游 `1.5.18`)。workspace `1.0.0`、preview-download `1.0.1` 和运行布局 7 保持不变。三个上游插件归档的 SHA-512 已核对并更新。
同步新 profile 和已有内置归档引用的迁移目标,保留自定义来源、插件启用选择、备份及锁文件同步机制。设置和 Office 补丁版本门禁对齐 rc.3,真实发布包的原生设置接口与 Office converter 导入入口检查通过。Chat Manager 的三处客户端适配仍精确匹配,新版真实源码补丁及语法检查通过;没有修改认证机制。mobile/free-search 的预发布 peer 例外仍只允许精确 rc.3 且满足 includePrerelease 范围。
本次 `node --test image/*.test.mjs` 共 46 项通过,包含新增的 runtime-7.9 实际插件版本引用迁移、备份及幂等回归。单元测试和源码核查不等同于完整运行兼容性证明;浏览器交互、真实搜索、设置保存、Office 实际转换、终端原生模块、容器离线启动及 ARM64 仍需本版本验收。未提交、推送或修改线上实例。
锁文件集成脚本默认验证 `dsh-web:runtime-7.9-amd64` → `dsh-web:runtime-7.10-amd64`,也可使用 `DSH_OLD_IMAGE` / `DSH_NEW_IMAGE` 指定已构建的镜像:`node image/lockfile-upgrade-smoke.mjs`。须先准备两个镜像;本次尚未完成该集成检查,不沿用旧发布的通过结论。
后续运行验收发现 preview-download 安装器仍限定 rc.2,已核对 rc.3 的操作插槽及 readBytes 接口并修正精确宿主门禁。新增安装器回归直接校验发布清单中的宿主版本可以完成打包调用、未经验证版本在写入前被拒绝;目前全部 47 项回归通过。
AMD64 镜像 `dsh-web:runtime-7.10-amd64` 已成功构建(ID `sha256:57d99cad32380dfc837fd1365814943b53f96abc07e84df154d02b7baa95f0bf`)。MiSans 下载首次超时,原来源重试后校验通过。禁网 UID 1000 新 profile 离线依赖安装、默认启动、未认证 HTTP 401、重启及锁文件哈希不变均通过。Office 实际 DOCX/PPTX/DOC/PPT 并发转换、已安装 DSH convertBytes 集成、输出/ZIP 限制、超时和清理通过。以上结果更新此前待验收状态;浏览器完整交互、真实搜索、设置保存、终端原生模块、旧镜像锁文件迁移和 ARM64 尚未验收,不代表可直接发布。
## runtime-7.9:DSH next 与启动配置迁移 ## runtime-7.9:DSH next 与启动配置迁移
锁文件同步修复:内置依赖迁移或补建 workspace 配置前,先持久化 `.dsh-lockfile-pending` 标记。仅带标记的启动安装显式传入 `--no-frozen-lockfile`,同步 `pnpm-lock.yaml`;成功后清除标记,后续启动恢复 CI 默认冻结检查。安装失败或超时保留标记并阻止应用启动,下次启动继续同步,不自动恢复旧 manifest。已有 manifest 备份机制保持不变。若旧版失败迁移已经留下“新 manifest+旧锁文件”且没有标记,应先恢复匹配的旧 profile 备份再升级;不要直接删除锁文件。 锁文件同步修复:内置依赖迁移或补建 workspace 配置前,先持久化 `.dsh-lockfile-pending` 标记。仅带标记的启动安装显式传入 `--no-frozen-lockfile`,同步 `pnpm-lock.yaml`;成功后清除标记,后续启动恢复 CI 默认冻结检查。安装失败或超时保留标记并阻止应用启动,下次启动继续同步,不自动恢复旧 manifest。已有 manifest 备份机制保持不变。若旧版失败迁移已经留下“新 manifest+旧锁文件”且没有标记,应先恢复匹配的旧 profile 备份再升级;不要直接删除锁文件。
@@ -92,7 +106,7 @@ Runtime 镜像包含 Debian Bookworm 的 Python 3、pip、venv(`python` 指向
## 发布清单 ## 发布清单
`release.json` 是发布入口:`schema_version` 为清单格式版本,`release_id` 标识一次发布,`version` 为精确 DSH 版本,`deployment_schema` 为运行布局版本,`platforms` 为支持的平台。当前待发布配置为 `runtime-7.9`、DSH `0.2.1-rc.2`、布局 7、`linux/amd64` 和 `linux/arm64`(aarch64,不含 32 位 ARM)。 `release.json` 是发布入口:`schema_version` 为清单格式版本,`release_id` 标识一次发布,`version` 为精确 DSH 版本,`deployment_schema` 为运行布局版本,`platforms` 为支持的平台。当前待验收配置为 `runtime-7.10`、DSH `0.2.1-rc.3`、布局 7、`linux/amd64` 和 `linux/arm64`(aarch64,不含 32 位 ARM)。
ARM64 验收状态:已确认 Node/Caddy 基础镜像提供 ARM64 版本,但尚未完成 ARM64 runtime 构建及运行验收。清单声明会开放面板的对应平台构建入口,不等于兼容性证明;验收完成前不要推送发布。 ARM64 验收状态:已确认 Node/Caddy 基础镜像提供 ARM64 版本,但尚未完成 ARM64 runtime 构建及运行验收。清单声明会开放面板的对应平台构建入口,不等于兼容性证明;验收完成前不要推送发布。
+1 -1
View File
@@ -14,7 +14,7 @@ export function initProfile(home, options) {
pnpm: { overrides: BUILTIN_OVERRIDES }, pnpm: { overrides: BUILTIN_OVERRIDES },
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dsh-web-mobile', 'dsh-free-search', 'dsh-mgr-workspace', 'dsh-chat-manager', 'dsh-preview-download'] } }, dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dsh-web-mobile', 'dsh-free-search', 'dsh-mgr-workspace', 'dsh-chat-manager', 'dsh-preview-download'] } },
}, null, 2), { flag: 'wx', mode: 0o600 }); }, null, 2), { flag: 'wx', mode: 0o600 });
console.log('dsh-mgr: new web profile includes dsh-web-mobile@3.1.0 and [email protected].2 and [email protected]6-dsh-mgr.1'); console.log('dsh-mgr: new web profile includes dsh-web-mobile@3.2.1 and [email protected].15 and [email protected]8-dsh-mgr.1');
} }
const migration = reconcileProfile(profile, options); const migration = reconcileProfile(profile, options);
console.log(migration.changed console.log(migration.changed
+4 -4
View File
@@ -25,15 +25,15 @@ test('new profile enables bundle once and preserves existing configuration', ()
assert.equal(JSON.parse(original).dependencies['dsh-preview-download'], 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-preview-download'], 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz');
assert.equal(JSON.parse(original).dsh.profile.bundles.filter(name => name === 'dsh-preview-download').length, 1); assert.equal(JSON.parse(original).dsh.profile.bundles.filter(name => name === 'dsh-preview-download').length, 1);
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-chat-manager')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-chat-manager'));
assert.equal(JSON.parse(original).dependencies['dsh-chat-manager'], 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-chat-manager'], 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.18-dsh-mgr.1.tgz');
assert.equal(JSON.parse(original).pnpm.overrides['dsh-chat-manager>clsx'], 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz'); assert.equal(JSON.parse(original).pnpm.overrides['dsh-chat-manager>clsx'], 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz');
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-web-mobile')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-web-mobile'));
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-free-search')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-free-search'));
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-mgr-workspace')); assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-mgr-workspace'));
assert.equal(JSON.parse(original).dependencies['dsh-mgr-workspace'], 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-mgr-workspace'], 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz');
assert.equal(JSON.parse(original).dependencies['dsh-free-search'], 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-free-search'], 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.15.tgz');
assert.equal(JSON.parse(original).pnpm.overrides['dsh-free-search>@deepseek-ai/schemastery'], 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz'); assert.equal(JSON.parse(original).pnpm.overrides['dsh-free-search>@deepseek-ai/schemastery'], 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz');
assert.equal(JSON.parse(original).dependencies['dsh-web-mobile'], 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz'); assert.equal(JSON.parse(original).dependencies['dsh-web-mobile'], 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.2.1.tgz');
const workspace = join(home, 'profiles/web/pnpm-workspace.yaml'); const workspace = join(home, 'profiles/web/pnpm-workspace.yaml');
assert.match(readFileSync(workspace, 'utf8'), /autoInstallPeers: false/); assert.match(readFileSync(workspace, 'utf8'), /autoInstallPeers: false/);
assert.match(readFileSync(workspace, 'utf8'), /nodeLinker: hoisted/); assert.match(readFileSync(workspace, 'utf8'), /nodeLinker: hoisted/);
@@ -67,7 +67,7 @@ test('chat-manager-only restored profile gets overrides and offline peer setting
try { try {
const profile = join(home, 'profiles/web'); const profile = join(home, 'profiles/web');
mkdirSync(profile, { recursive: true }); mkdirSync(profile, { recursive: true });
const manifest = JSON.stringify({ dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz' } }); const manifest = JSON.stringify({ dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.18-dsh-mgr.1.tgz' } });
writeFileSync(join(profile, 'package.json'), manifest); writeFileSync(join(profile, 'package.json'), manifest);
initProfile(home, archives(home)); initProfile(home, archives(home));
assert.deepEqual(JSON.parse(readFileSync(join(profile, 'package.json'))), { ...JSON.parse(manifest), pnpm: { overrides: { 'dsh-chat-manager>clsx': BUILTIN_OVERRIDES['dsh-chat-manager>clsx'] } } }); assert.deepEqual(JSON.parse(readFileSync(join(profile, 'package.json'))), { ...JSON.parse(manifest), pnpm: { overrides: { 'dsh-chat-manager>clsx': BUILTIN_OVERRIDES['dsh-chat-manager>clsx'] } } });
+6 -6
View File
@@ -5,12 +5,12 @@ import { createRequire } from 'node:module';
import { patchClient } from './patch-chat-manager.mjs'; import { patchClient } from './patch-chat-manager.mjs';
const root = '/usr/local/lib/dsh-mgr'; const root = '/usr/local/lib/dsh-mgr';
const version = '1.5.16'; const version = '1.5.18';
const patchedVersion = '1.5.16-dsh-mgr.1'; const patchedVersion = '1.5.18-dsh-mgr.1';
const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json'; const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json';
const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-chat-manager'; const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-chat-manager';
const packages = [ const packages = [
['dsh-chat-manager', version, 'https://registry.npmjs.org/dsh-chat-manager/-/dsh-chat-manager-1.5.16.tgz', 'tPE79Q/FKA9+MxI/GAcRACyN48tm9vxqM9kb4iMRDgP4EMaS39Ipbe7+4XisZcYIR6QjndBc8mQjlxb/gnFzuQ=='], ['dsh-chat-manager', version, 'https://registry.npmjs.org/dsh-chat-manager/-/dsh-chat-manager-1.5.18.tgz', 'kkGog/h162duGKjeSx5NgN/KRzsniIPXeUTTcAMyTM9ci0eA/KdORUpR8v/AqWPBNH/CAg3F1+OgXUpbI4vkLQ=='],
['clsx', '2.1.1', 'https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz', 'eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=='], ['clsx', '2.1.1', 'https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz', 'eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=='],
]; ];
mkdirSync(root, { recursive: true }); mkdirSync(root, { recursive: true });
@@ -30,7 +30,7 @@ const require = createRequire(hostManifest);
const host = JSON.parse(readFileSync(hostManifest)); const host = JSON.parse(readFileSync(hostManifest));
const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver'); const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver');
for (const [name, range] of Object.entries(pkg.peerDependencies || {})) { for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
// 1.5.16 retains this optional legacy peer but its runtime no longer imports it. // 1.5.18 retains this optional legacy peer but its runtime no longer imports it.
if (name === '@deepseek-ai/dsh-invariants' && pkg.peerDependenciesMeta?.[name]?.optional) { if (name === '@deepseek-ai/dsh-invariants' && pkg.peerDependenciesMeta?.[name]?.optional) {
for (const file of ['src/index.js', 'src/host/archive-manager.mjs', 'src/host/delete-session.mjs', 'lib/client.js']) { for (const file of ['src/index.js', 'src/host/archive-manager.mjs', 'src/host/delete-session.mjs', 'lib/client.js']) {
if (readFileSync(destination + '/' + file, 'utf8').includes(name)) throw new Error('legacy invariants peer is used again'); if (readFileSync(destination + '/' + file, 'utf8').includes(name)) throw new Error('legacy invariants peer is used again');
@@ -38,11 +38,11 @@ for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
continue; continue;
} }
if (name === 'react') { if (name === 'react') {
if (host.version !== '0.2.1-rc.2') throw new Error('unverified browser React host: ' + host.version); if (host.version !== '0.2.1-rc.3') throw new Error('unverified browser React host: ' + host.version);
continue; continue;
} }
// New web hosts bundle this pure UI module in the browser bootstrap. // New web hosts bundle this pure UI module in the browser bootstrap.
if (['@deepseek-ai/dsh-client-ui-primitives', '@deepseek-ai/dsh-client-ui-slots'].includes(name) && host.version === '0.2.1-rc.2') { if (['@deepseek-ai/dsh-client-ui-primitives', '@deepseek-ai/dsh-client-ui-slots'].includes(name) && host.version === '0.2.1-rc.3') {
const frontend = require.resolve('@deepseek-ai/dsh-web-frontend/package.json'); const frontend = require.resolve('@deepseek-ai/dsh-web-frontend/package.json');
const assets = frontend.slice(0, -'package.json'.length) + 'dist/assets'; const assets = frontend.slice(0, -'package.json'.length) + 'dist/assets';
const frontendVersion = JSON.parse(readFileSync(frontend)).version; const frontendVersion = JSON.parse(readFileSync(frontend)).version;
+7 -7
View File
@@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const root = '/usr/local/lib/dsh-mgr'; const root = '/usr/local/lib/dsh-mgr';
const packages = [ const packages = [
['dsh-free-search', '0.9.2', 'https://registry.npmjs.org/dsh-free-search/-/dsh-free-search-0.9.2.tgz', 'pFT/ngMcA7hxjbubNbl9iIBmf0zq9I6seVFcHVR8jUckCgoqoX92vjAmVIGA5mzSKELe/CFkq7uCnXtdTIg92Q=='], ['dsh-free-search', '0.9.15', 'https://registry.npmjs.org/dsh-free-search/-/dsh-free-search-0.9.15.tgz', 'T6YxSR4NOK3c7ivdqDxrOdgdORGYBFmGbM7oZNpFUVZg2KIbU0f4TSOO99wjG0NnSrwqjdc1i/yK8pviZHtVhA=='],
['@deepseek-ai/schemastery', '3.18.4', 'https://registry.npmjs.org/@deepseek-ai/schemastery/-/schemastery-3.18.4.tgz', 'SSXO6tYuyrIqKVbmOnIq0s+riUywYzouFMcnBluHF9n4KMo2G8HvEzhCYj6pj2617/glOjbJuVInJ9hfONsjkg=='], ['@deepseek-ai/schemastery', '3.18.4', 'https://registry.npmjs.org/@deepseek-ai/schemastery/-/schemastery-3.18.4.tgz', 'SSXO6tYuyrIqKVbmOnIq0s+riUywYzouFMcnBluHF9n4KMo2G8HvEzhCYj6pj2617/glOjbJuVInJ9hfONsjkg=='],
['@deepseek-ai/cosmokit', '1.8.5', 'https://registry.npmjs.org/@deepseek-ai/cosmokit/-/cosmokit-1.8.5.tgz', 'LXsrlem9z8dq4sLflj2yuCuYX7KqhdzF5hZly3eZyuTo8d6oDSOXuEgC5DbQWKW+lksm6zmOutQLsP/ma6wR6A=='], ['@deepseek-ai/cosmokit', '1.8.5', 'https://registry.npmjs.org/@deepseek-ai/cosmokit/-/cosmokit-1.8.5.tgz', 'LXsrlem9z8dq4sLflj2yuCuYX7KqhdzF5hZly3eZyuTo8d6oDSOXuEgC5DbQWKW+lksm6zmOutQLsP/ma6wR6A=='],
['@standard-schema/spec', '1.1.0', 'https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz', 'l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=='], ['@standard-schema/spec', '1.1.0', 'https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz', 'l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=='],
@@ -20,21 +20,21 @@ for (const [name, version, url, integrity] of packages) {
const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json'; const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json';
const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-free-search'; const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-free-search';
mkdirSync(destination, { recursive: true }); mkdirSync(destination, { recursive: true });
execFileSync('tar', ['-xzf', `${root}/dsh-free-search-0.9.2.tgz`, '--strip-components=1', '--no-same-owner', '-C', destination]); execFileSync('tar', ['-xzf', `${root}/dsh-free-search-0.9.15.tgz`, '--strip-components=1', '--no-same-owner', '-C', destination]);
execFileSync('chmod', ['-R', 'a+rX', destination]); execFileSync('chmod', ['-R', 'a+rX', destination]);
const pkg = JSON.parse(readFileSync(destination + '/package.json')); const pkg = JSON.parse(readFileSync(destination + '/package.json'));
if (pkg.name !== 'dsh-free-search' || pkg.version !== '0.9.2' || JSON.stringify(pkg.dependencies) !== JSON.stringify({ '@deepseek-ai/schemastery': '^3.18.4' })) throw new Error('unexpected free-search metadata'); if (pkg.name !== 'dsh-free-search' || pkg.version !== '0.9.15' || JSON.stringify(pkg.dependencies) !== JSON.stringify({ '@deepseek-ai/schemastery': '^3.18.4' })) throw new Error('unexpected free-search metadata');
const require = createRequire(hostManifest); const require = createRequire(hostManifest);
const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver'); const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver');
for (const [name, range] of Object.entries({ ...pkg.peerDependencies, ...pkg.dependencies })) { for (const [name, range] of Object.entries({ ...pkg.peerDependencies, ...pkg.dependencies })) {
if (name === 'react') { if (name === 'react') {
if (!['0.2.0-rc.1', '0.2.1-rc.2'].includes(require('./package.json').version)) throw new Error('unverified browser React host'); if (require('./package.json').version !== '0.2.1-rc.3') throw new Error('unverified browser React host');
continue; continue;
} }
const peer = require(name + '/package.json'); const peer = require(name + '/package.json');
if (!(semver.satisfies(peer.version, range) || (peer.version === "0.2.1-rc.2" && name.startsWith("@deepseek-ai/dsh-") && semver.satisfies(peer.version, range, { includePrerelease: true })))) throw new Error('incompatible free-search host dependency: ' + name); if (!(semver.satisfies(peer.version, range) || (peer.version === "0.2.1-rc.3" && name.startsWith("@deepseek-ai/dsh-") && semver.satisfies(peer.version, range, { includePrerelease: true })))) throw new Error('incompatible free-search host dependency: ' + name);
} }
const host = JSON.parse(readFileSync(hostManifest)); const host = JSON.parse(readFileSync(hostManifest));
host.dependencies = { ...host.dependencies, 'dsh-free-search': '0.9.2' }; host.dependencies = { ...host.dependencies, 'dsh-free-search': '0.9.15' };
writeFileSync(hostManifest, JSON.stringify(host, null, 2)); writeFileSync(hostManifest, JSON.stringify(host, null, 2));
console.log('Installed [email protected].2 and pinned offline dependency archives (SHA-512 verified; no lifecycle scripts)'); console.log('Installed [email protected].15 and pinned offline dependency archives (SHA-512 verified; no lifecycle scripts)');
+1 -1
View File
@@ -9,7 +9,7 @@ const replacement = 'const persistence = "host"; /* dsh-mgr: authenticated remot
// Fail closed on upstream changes instead of silently shipping a broken UI. // Fail closed on upstream changes instead of silently shipping a broken UI.
export function patchSettings(source, version) { export function patchSettings(source, version) {
// Native host settings supersede the old persistence patch. // Native host settings supersede the old persistence patch.
if (version === "0.2.1-rc.2") { if (version === "0.2.1-rc.3") {
if (source.includes(target) || !source.includes("this.ctx.remote.settings.describe()") || !source.includes("this.ctx.remote.settings.mutate(")) throw new Error("expected native host settings API"); if (source.includes(target) || !source.includes("this.ctx.remote.settings.describe()") || !source.includes("this.ctx.remote.settings.mutate(")) throw new Error("expected native host settings API");
return source; return source;
} }
+3 -3
View File
@@ -6,9 +6,9 @@ import { patchSettings, target } from './install-host-settings.mjs';
test("next host settings API remains unchanged", () => { test("next host settings API remains unchanged", () => {
const source = "this.ctx.remote.settings.describe(); this.ctx.remote.settings.mutate(namespace, ops, revision);"; const source = "this.ctx.remote.settings.describe(); this.ctx.remote.settings.mutate(namespace, ops, revision);";
assert.equal(patchSettings(source, "0.2.1-rc.2"), source); assert.equal(patchSettings(source, "0.2.1-rc.3"), source);
assert.throws(() => patchSettings(target, "0.2.1-rc.2"), /native host settings/); assert.throws(() => patchSettings(target, "0.2.1-rc.3"), /native host settings/);
assert.throws(() => patchSettings("", "0.2.1-rc.2"), /native host settings/); assert.throws(() => patchSettings("", "0.2.1-rc.3"), /native host settings/);
}); });
test('settings use host persistence on domains without changing loopback facts', () => { test('settings use host persistence on domains without changing loopback facts', () => {
+7 -7
View File
@@ -2,15 +2,15 @@ import { createHash } from 'node:crypto';
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'; import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
const version = '3.1.0'; const version = '3.2.1';
const integrity = 'dvRn0p0Y54RQErNRmW8z6K7JQJpo2euuMcgTvfJtgEMUJecs9Mcg4jqUNbmHWeMyijmZMUes/IRbzopDxKIdXA=='; const integrity = '2XHbebwr78ujeJVt3wqffSp7/LEnLqUajawnL0TWNGG6LcAimAm8q8GJdh28c7gdRs6BYAGxfF/TatA5E6xZ+A==';
const response = await fetch('https://registry.npmjs.org/dsh-web-mobile/-/dsh-web-mobile-3.1.0.tgz', { signal: AbortSignal.timeout(120000) }); const response = await fetch('https://registry.npmjs.org/dsh-web-mobile/-/dsh-web-mobile-3.2.1.tgz', { signal: AbortSignal.timeout(120000) });
if (!response.ok) throw new Error('mobile plugin download failed'); if (!response.ok) throw new Error('mobile plugin download failed');
const data = Buffer.from(await response.arrayBuffer()); const data = Buffer.from(await response.arrayBuffer());
if (createHash('sha512').update(data).digest('base64') !== integrity) throw new Error('mobile plugin integrity mismatch'); if (createHash('sha512').update(data).digest('base64') !== integrity) throw new Error('mobile plugin integrity mismatch');
const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-web-mobile'; const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-web-mobile';
mkdirSync(destination, { recursive: true }); mkdirSync(destination, { recursive: true });
const archive = '/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz'; const archive = '/usr/local/lib/dsh-mgr/dsh-web-mobile-3.2.1.tgz';
writeFileSync(archive, data, { mode: 0o644 }); writeFileSync(archive, data, { mode: 0o644 });
execFileSync('tar', ['-xzf', archive, '--strip-components=1', '--no-same-owner', '-C', destination]); execFileSync('tar', ['-xzf', archive, '--strip-components=1', '--no-same-owner', '-C', destination]);
// Published tar modes can be owner-only; runtime is UID 1000, not build root. // Published tar modes can be owner-only; runtime is UID 1000, not build root.
@@ -24,7 +24,7 @@ for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
// Limit that exception to the upstream-declared, tested RC hosts. // Limit that exception to the upstream-declared, tested RC hosts.
if (name === 'react') { if (name === 'react') {
const host = require('./package.json').version; const host = require('./package.json').version;
if (!['0.2.0-rc.1', '0.2.1-rc.2'].includes(host)) throw new Error('unverified browser React host: ' + host); if (host !== '0.2.1-rc.3') throw new Error('unverified browser React host: ' + host);
continue; continue;
} }
let peer; let peer;
@@ -33,7 +33,7 @@ for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
if (pkg.peerDependenciesMeta?.[name]?.optional) continue; if (pkg.peerDependenciesMeta?.[name]?.optional) continue;
throw new Error('missing required mobile peer: ' + name); throw new Error('missing required mobile peer: ' + name);
} }
if (!(semver.satisfies(peer.version, range) || (peer.version === "0.2.1-rc.2" && name.startsWith("@deepseek-ai/dsh-") && semver.satisfies(peer.version, range, { includePrerelease: true })))) throw new Error('incompatible mobile peer: ' + name + '@' + peer.version); if (!(semver.satisfies(peer.version, range) || (peer.version === "0.2.1-rc.3" && name.startsWith("@deepseek-ai/dsh-") && semver.satisfies(peer.version, range, { includePrerelease: true })))) throw new Error('incompatible mobile peer: ' + name + '@' + peer.version);
} }
// DSH runtime resolution traverses the installation manifest, not arbitrary // DSH runtime resolution traverses the installation manifest, not arbitrary
// directories in node_modules. Declare the baked package in that graph. // directories in node_modules. Declare the baked package in that graph.
@@ -41,4 +41,4 @@ const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json'
const host = JSON.parse(readFileSync(hostManifest)); const host = JSON.parse(readFileSync(hostManifest));
host.dependencies = { ...host.dependencies, 'dsh-web-mobile': version }; host.dependencies = { ...host.dependencies, 'dsh-web-mobile': version };
writeFileSync(hostManifest, JSON.stringify(host, null, 2)); writeFileSync(hostManifest, JSON.stringify(host, null, 2));
console.log('Installed dsh-web-mobile@3.1.0 (SHA-512 verified; no lifecycle scripts)'); console.log('Installed dsh-web-mobile@3.2.1 (SHA-512 verified; no lifecycle scripts)');
+1 -1
View File
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
export const target = 'import { createConverter } from "@deepseek-ai/libreoffice-kit";'; export const target = 'import { createConverter } from "@deepseek-ai/libreoffice-kit";';
export function patchOffice(source, hostVersion, packageVersion) { export function patchOffice(source, hostVersion, packageVersion) {
if (hostVersion !== '0.2.1-rc.2' || packageVersion !== '0.2.1-rc.2') throw new Error('unsupported Office preview version'); if (hostVersion !== '0.2.1-rc.3' || packageVersion !== '0.2.1-rc.3') throw new Error('unsupported Office preview version');
if (source.split(target).length !== 2) throw new Error('expected exactly one original converter import'); if (source.split(target).length !== 2) throw new Error('expected exactly one original converter import');
return source.replace(target, 'import { createConverter } from "./dsh-native-office.mjs";'); return source.replace(target, 'import { createConverter } from "./dsh-native-office.mjs";');
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { cpSync, mkdirSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
const root = '/usr/local/lib/dsh-mgr'; const root = '/usr/local/lib/dsh-mgr';
const host = JSON.parse(readFileSync('/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json')); const host = JSON.parse(readFileSync('/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json'));
if (host.version !== '0.2.1-rc.2') throw new Error('Unverified preview download host'); if (host.version !== '0.2.1-rc.3') throw new Error('Unverified preview download host');
mkdirSync(root + '/preview-download-package', { recursive: true }); mkdirSync(root + '/preview-download-package', { recursive: true });
cpSync(new URL('./preview-download', import.meta.url), root + '/preview-download-package/package', { recursive: true }); cpSync(new URL('./preview-download', import.meta.url), root + '/preview-download-package/package', { recursive: true });
execFileSync('tar', ['-czf', root + '/dsh-preview-download-1.0.1.tgz', '-C', root + '/preview-download-package', 'package']); execFileSync('tar', ['-czf', root + '/dsh-preview-download-1.0.1.tgz', '-C', root + '/preview-download-package', 'package']);
+6 -4
View File
@@ -3,21 +3,23 @@ import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
const oldImage = process.env.DSH_OLD_IMAGE || 'dsh-web:runtime-7.9-amd64';
const newImage = process.env.DSH_NEW_IMAGE || 'dsh-web:runtime-7.10-amd64';
const volume = `dsh-lockfile-smoke-${randomUUID()}`; const volume = `dsh-lockfile-smoke-${randomUUID()}`;
const docker = args => execFileSync('docker', args, { encoding: 'utf8', timeout: 180000, maxBuffer: 8 * 1024 * 1024 }); const docker = args => execFileSync('docker', args, { encoding: 'utf8', timeout: 180000, maxBuffer: 8 * 1024 * 1024 });
const run = (image, args, mounts = []) => docker(['run', '--rm', '--network', 'none', '-v', `${volume}:/workspace`, ...mounts, '--entrypoint', 'bash', image, '-ec', args]); const run = (image, args, mounts = []) => docker(['run', '--rm', '--network', 'none', '-v', `${volume}:/workspace`, ...mounts, '--entrypoint', 'bash', image, '-ec', args]);
docker(['volume', 'create', volume]); docker(['volume', 'create', volume]);
try { try {
run('dsh-upgrade-test:runtime-7.8', 'node /usr/local/lib/dsh-mgr/init-profile.mjs; CI=true npm_config_offline=true dsh plugin --profile web install --ignore-scripts; test -s /workspace/profiles/web/pnpm-lock.yaml'); run(oldImage, 'node /usr/local/lib/dsh-mgr/init-profile.mjs; CI=true npm_config_offline=true dsh plugin --profile web install --ignore-scripts; test -s /workspace/profiles/web/pnpm-lock.yaml');
const mounts = ['init-profile.mjs', 'reconcile-profile.mjs'].flatMap(name => ['-v', `${fileURLToPath(new URL(name, import.meta.url))}:/usr/local/lib/dsh-mgr/${name}:ro`]); const mounts = ['init-profile.mjs', 'reconcile-profile.mjs'].flatMap(name => ['-v', `${fileURLToPath(new URL(name, import.meta.url))}:/usr/local/lib/dsh-mgr/${name}:ro`]);
mounts.push('-v', `${fileURLToPath(new URL('entrypoint.sh', import.meta.url))}:/usr/local/bin/dsh-entrypoint:ro`); mounts.push('-v', `${fileURLToPath(new URL('entrypoint.sh', import.meta.url))}:/usr/local/bin/dsh-entrypoint:ro`);
const output = run('dsh-web:runtime-7.9-amd64', 'bash /usr/local/bin/dsh-entrypoint --version; test ! -e /workspace/profiles/web/.dsh-lockfile-pending; CI=true npm_config_offline=true dsh plugin --profile web install --ignore-scripts', mounts); const output = run(newImage, 'bash /usr/local/bin/dsh-entrypoint --version; test ! -e /workspace/profiles/web/.dsh-lockfile-pending; CI=true npm_config_offline=true dsh plugin --profile web install --ignore-scripts', mounts);
assert.doesNotMatch(output, /ERR_PNPM_OUTDATED_LOCKFILE/); assert.doesNotMatch(output, /ERR_PNPM_OUTDATED_LOCKFILE/);
const again = run('dsh-web:runtime-7.9-amd64', 'sha256sum /workspace/profiles/web/pnpm-lock.yaml; bash /usr/local/bin/dsh-entrypoint --version; sha256sum /workspace/profiles/web/pnpm-lock.yaml', mounts); const again = run(newImage, 'sha256sum /workspace/profiles/web/pnpm-lock.yaml; bash /usr/local/bin/dsh-entrypoint --version; sha256sum /workspace/profiles/web/pnpm-lock.yaml', mounts);
const hashes = again.match(/^[a-f0-9]{64} .*pnpm-lock.yaml$/gm); const hashes = again.match(/^[a-f0-9]{64} .*pnpm-lock.yaml$/gm);
assert.equal(hashes?.length, 2, again); assert.equal(hashes?.length, 2, again);
assert.equal(hashes[0], hashes[1]); assert.equal(hashes[0], hashes[1]);
console.log('PASS: real 7.8 lockfile → 7.9 migration, frozen reinstall, unchanged lockfile on restart'); console.log(`PASS: ${oldImage} → ${newImage} migration, frozen reinstall, unchanged lockfile on restart`);
} finally { } finally {
docker(['volume', 'rm', volume]); docker(['volume', 'rm', volume]);
} }
+4 -4
View File
@@ -7,11 +7,11 @@ import { createConverter, runProcess } from './native-office.mjs';
import { patchOffice, target } from './install-office-preview.mjs'; import { patchOffice, target } from './install-office-preview.mjs';
test('patch is exact, version gated and refuses repeat application', () => { test('patch is exact, version gated and refuses repeat application', () => {
const patched = patchOffice(target, '0.2.1-rc.2', '0.2.1-rc.2'); const patched = patchOffice(target, '0.2.1-rc.3', '0.2.1-rc.3');
assert.match(patched, /dsh-native-office/); assert.match(patched, /dsh-native-office/);
for (const source of ['', target + target, patched]) assert.throws(() => patchOffice(source, '0.2.1-rc.2', '0.2.1-rc.2')); for (const source of ['', target + target, patched]) assert.throws(() => patchOffice(source, '0.2.1-rc.3', '0.2.1-rc.3'));
assert.throws(() => patchOffice(target, 'next', '0.2.1-rc.2')); assert.throws(() => patchOffice(target, 'next', '0.2.1-rc.3'));
assert.throws(() => patchOffice(target, '0.2.1-rc.2', 'next')); assert.throws(() => patchOffice(target, '0.2.1-rc.3', 'next'));
}); });
test('process cancellation, missing binary and failure are classified', async () => { test('process cancellation, missing binary and failure are classified', async () => {
+1 -1
View File
@@ -13,7 +13,7 @@ async function dshChatJson(response) {
export const clientHelpers = dshChatFetch.toString() + String.fromCharCode(10) + dshChatJson.toString() + String.fromCharCode(10); export const clientHelpers = dshChatFetch.toString() + String.fromCharCode(10) + dshChatJson.toString() + String.fromCharCode(10);
export function patchClient(source, version) { export function patchClient(source, version) {
if (version !== '1.5.16') throw new Error('unsupported chat-manager version'); if (version !== '1.5.18') throw new Error('unsupported chat-manager version');
if (source.includes('dshChatFetch') || source.split('await fetch(').length !== 4 if (source.includes('dshChatFetch') || source.split('await fetch(').length !== 4
|| source.split('await response.json()').length !== 4 || source.split('await response.json()').length !== 4
|| source.split('/plugins/dsh-session-delete/').length !== 4) { || source.split('/plugins/dsh-session-delete/').length !== 4) {
+14 -4
View File
@@ -25,10 +25,20 @@ test('non-JSON errors report HTTP status without exposing response content', asy
assert.equal(body.error.code, 'forbidden'); assert.equal(body.error.code, 'forbidden');
}); });
test('patch rejects unsupported or changed clients and double patching', () => { test('patch rejects unsupported or changed clients and double patching', () => {
const source = Array(3).fill("await fetch('/plugins/dsh-session-delete/delete'); await response.json()").join(';'); const source = [
const patched = patchClient(source, '1.5.16'); "await fetch('/plugins/dsh-session-delete/delete', options); await response.json()",
'await fetch(`/plugins/dsh-session-delete/${action}`, options); await response.json()',
"await fetch('/plugins/dsh-session-delete/archive-search', options); await response.json()",
].join(';');
const patched = patchClient(source, '1.5.18');
assert.equal(patched.split('await dshChatFetch(').length, 4); assert.equal(patched.split('await dshChatFetch(').length, 4);
assert.equal(patched.slice(clientHelpers.length).replaceAll('await dshChatFetch(', 'await fetch(')
.replaceAll('await dshChatJson(response)', 'await response.json()'), source);
assert.throws(() => patchClient(source, '1.5.16'), /unsupported/);
assert.throws(() => patchClient(source, '1.5.13'), /unsupported/); assert.throws(() => patchClient(source, '1.5.13'), /unsupported/);
assert.throws(() => patchClient('', '1.5.16'), /unexpected/); assert.throws(() => patchClient('', '1.5.18'), /unexpected/);
assert.throws(() => patchClient(patched, '1.5.16'), /unexpected/); assert.throws(() => patchClient(source.replace('await fetch(', 'fetch('), '1.5.18'), /unexpected/);
assert.throws(() => patchClient(source.replace('await response.json()', 'response.json()'), '1.5.18'), /unexpected/);
assert.throws(() => patchClient(source.replace('/plugins/dsh-session-delete/', '/plugins/changed/'), '1.5.18'), /unexpected/);
assert.throws(() => patchClient(patched, '1.5.18'), /unexpected/);
}); });
+24
View File
@@ -3,6 +3,30 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm'; import { runInNewContext } from 'node:vm';
test('installer accepts the release host and rejects unverified hosts before writing', () => {
const source = readFileSync(new URL('./install-preview-download.mjs', import.meta.url), 'utf8')
.replace(/^import .*;$/gm, '').replaceAll('import.meta.url', JSON.stringify(import.meta.url));
const release = JSON.parse(readFileSync(new URL('../release.json', import.meta.url)));
for (const version of [release.version, '0.2.1-rc.2', '0.2.1-rc.4', 'next']) {
const calls = [];
const run = () => runInNewContext(source, {
URL, readFileSync: () => JSON.stringify({ version }),
mkdirSync: (...args) => calls.push(['mkdir', ...args]),
cpSync: (...args) => calls.push(['copy', ...args]),
execFileSync: (...args) => calls.push(['exec', ...args]),
});
if (version === release.version) {
run();
assert.deepEqual(calls.map(call => call[0]), ['mkdir', 'copy', 'exec']);
assert.equal(calls[2][1], 'tar');
assert.equal(calls[2][2][1], '/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz');
} else {
assert.throws(run, /Unverified preview download host/);
assert.equal(calls.length, 0);
}
}
});
function load() { function load() {
let plugin; let plugin;
runInNewContext(readFileSync(new URL('./preview-download/client.js', import.meta.url), 'utf8'), { runInNewContext(readFileSync(new URL('./preview-download/client.js', import.meta.url), 'utf8'), {
+3 -3
View File
@@ -4,10 +4,10 @@ import { randomUUID } from 'node:crypto';
export const BUILTIN_DEPENDENCIES = Object.freeze({ export const BUILTIN_DEPENDENCIES = Object.freeze({
'dsh-preview-download': 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz', 'dsh-preview-download': 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz',
'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz', 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.18-dsh-mgr.1.tgz',
'dsh-mgr-workspace': 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz', 'dsh-mgr-workspace': 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz',
'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz', 'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.2.1.tgz',
'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.tgz', 'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.15.tgz',
}); });
export const BUILTIN_OVERRIDES = Object.freeze({ export const BUILTIN_OVERRIDES = Object.freeze({
'dsh-chat-manager>clsx': 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz', 'dsh-chat-manager>clsx': 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz',
+18
View File
@@ -8,6 +8,24 @@ import { BUILTIN_DEPENDENCIES, BUILTIN_OVERRIDES, reconcileManifest, reconcilePr
import { initProfile } from './init-profile.mjs'; import { initProfile } from './init-profile.mjs';
const old = name => 'file:/usr/local/lib/dsh-mgr/' + name + '-0.0.1-rc.1+build.2.tgz'; const old = name => 'file:/usr/local/lib/dsh-mgr/' + name + '-0.0.1-rc.1+build.2.tgz';
test('runtime-7.9 plugin references migrate to runtime-7.10 without changing bundle choices', t => {
const bundles = ['dsh-web-mobile', 'dsh-chat-manager'];
const f = fixture(t, {
dependencies: {
'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz',
'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.tgz',
'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz',
},
dsh: { profile: { bundles } },
});
const result = f.run();
assert.equal(result.changed, true);
assert.equal(readFileSync(result.backupPath, 'utf8'), f.original);
const current = JSON.parse(readFileSync(f.path));
for (const name of Object.keys(current.dependencies)) assert.equal(current.dependencies[name], BUILTIN_DEPENDENCIES[name]);
assert.deepEqual(current.dsh.profile.bundles, bundles);
assert.deepEqual(f.run(), { changed: false });
});
function fixture(t, manifest) { function fixture(t, manifest) {
const root = mkdtempSync(join(tmpdir(), 'dsh-reconcile-')); const root = mkdtempSync(join(tmpdir(), 'dsh-reconcile-'));
t.after(() => rmSync(root, { recursive: true, force: true })); t.after(() => rmSync(root, { recursive: true, force: true }));
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"schema_version": 1, "schema_version": 1,
"release_id": "runtime-7.9", "release_id": "runtime-7.10",
"version": "0.2.1-rc.2", "version": "0.2.1-rc.3",
"deployment_schema": 7, "deployment_schema": 7,
"platforms": ["linux/amd64", "linux/arm64"] "platforms": ["linux/amd64", "linux/arm64"]
} }