release: add runtime-7.1 build files
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# DSH Runtime Release
|
||||
|
||||
`dockerfile` 分支只维护发布清单和 runtime 镜像构建文件,不包含管理面板源码或部署配置。
|
||||
|
||||
## 发布清单
|
||||
|
||||
`release.json` 是发布入口:`schema_version` 为清单格式版本,`release_id` 标识一次发布,`version` 为精确 DSH 版本,`deployment_schema` 为运行布局版本,`platforms` 为支持的平台。当前为 `runtime-7.1`、DSH `0.2.0-rc.2`、布局 7、`linux/amd64`。
|
||||
|
||||
此分支是可信代码来源,不是仅供读取的数据源:Dockerfile 和安装脚本会在构建时执行。只使用经过审核的提交,不要将不可信分支、PR 或任意远端内容直接用于构建。清单本身不代表镜像已经完成运行验收。
|
||||
|
||||
## 本地构建与检查
|
||||
|
||||
需要 Docker、Node.js,以及拉取基础镜像和构建依赖的网络。从仓库根目录执行,版本和平台直接取自清单,不跟踪 latest:
|
||||
|
||||
~~~sh
|
||||
DSH_VERSION="$(node -p "require('./release.json').version")"
|
||||
PLATFORM="$(node -p "require('./release.json').platforms[0]")"
|
||||
RELEASE_ID="$(node -p "require('./release.json').release_id")"
|
||||
node --test image/*.test.mjs
|
||||
docker build --platform "$PLATFORM" \
|
||||
--build-arg DSH_VERSION="$DSH_VERSION" \
|
||||
-t "dsh-web:$RELEASE_ID" ./image
|
||||
~~~
|
||||
|
||||
`image/` 是构建上下文。构建只生成本地镜像,不推送镜像,也不自动发布分支。保留 `image/init-profile.test.mjs` 作为独立 runtime 回归测试,覆盖初始化幂等性、保留已有配置和拒绝符号链接目录;它不依赖面板,且被 `image/.dockerignore` 排除在构建上下文之外。此测试不能替代真实容器启动、插件和持久化验收。
|
||||
|
||||
## 布局 7 合约
|
||||
|
||||
- 以 UID/GID 1000 运行;实例持久目录挂载到 `/workspace`,必须允许该身份读写。`DSH_HOME`、`HOME` 和工作目录均为 `/workspace`,XDG 数据与配置也位于其中。
|
||||
- `/data/dsh-home` 是指向 `/workspace` 的兼容符号链接;不要用旧 home 挂载覆盖该路径。`/shared` 不是布局 7 的持久化入口。
|
||||
- DSH 监听 loopback 3080,Caddy 在容器 8080 转发;只通过受信入口访问,不直接公开 runtime 端口,不挂载 Docker socket。原生模块需要可执行的 `/tmp`,可保留 nosuid/nodev 与容量限制。
|
||||
- 新 profile 默认启用固定版本的 mobile、free-search 和 workspace bundle,默认工作区为 `/workspace`。已有非空 profile 不自动注入或覆盖;升级镜像不会强制改变已有插件选择。
|
||||
- 启动前协调插件依赖,失败则不启动应用;仅含内置固定依赖的 profile 使用离线安装。用户添加插件后可能需要网络。
|
||||
|
||||
## 手动发布
|
||||
|
||||
1. 审核 runtime 资产变更及其可信来源,选择精确 DSH 版本,更新 `release.json`。每次发布变更都必须使用新的 `release_id`;布局合约变化时同步更新 `deployment_schema`、镜像及调用方适配,不能只修改数字。
|
||||
2. 运行上述测试与本地构建,并完成目标平台的容器启动、插件、持久化与兼容验收;检查 `git diff --check` 和最终差异。
|
||||
3. 维护者确认后,**手动提交并手动推送** `dockerfile` 分支到已核实的发布远端。修改清单或本地构建不会自动 commit/push,也不会自动更新运行中的实例。
|
||||
|
||||
## 文件范围
|
||||
|
||||
- `release.json`:发布清单。
|
||||
- `README.md`:本分支说明。
|
||||
- `image/Dockerfile`、`image/Caddyfile`、`image/entrypoint.sh`、`image/.dockerignore`:镜像构建与启动。
|
||||
- `image/install-*.mjs`、`image/init-profile.mjs`:内置 runtime 资产安装与 profile 初始化。
|
||||
- `image/init-profile.test.mjs`:保留的 profile 初始化回归测试。
|
||||
- `image/entrypoint.test.mjs`:从面板测试迁移的 runtime 启动失败保护与镜像约束测试,需要 Bash;不依赖 Go 或面板。
|
||||
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
**/.git
|
||||
.env
|
||||
.env.*
|
||||
**/.env
|
||||
**/.env.*
|
||||
node_modules
|
||||
**/node_modules
|
||||
*.test.mjs
|
||||
embed.go
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
:8080 {
|
||||
reverse_proxy 127.0.0.1:3080
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
FROM caddy:2.10.2 AS proxy
|
||||
FROM node:24-bookworm-slim
|
||||
ARG DSH_VERSION
|
||||
# Keep the historical dependency-layer cache key; the template revision is set below.
|
||||
ARG RECIPE_REV=3
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates git procps && rm -rf /var/lib/apt/lists/*
|
||||
RUN test -n "$DSH_VERSION" && npm install --global --foreground-scripts --loglevel=verbose --registry=https://registry.npmjs.org "@deepseek-ai/dsh@${DSH_VERSION}"
|
||||
RUN npm install --global --ignore-scripts --registry=https://registry.npmjs.org [email protected] && test "$(pnpm --version)" = "10.11.0"
|
||||
COPY install-mobile.mjs /usr/local/lib/dsh-mgr/install-mobile.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-mobile.mjs
|
||||
COPY install-free-search.mjs /usr/local/lib/dsh-mgr/install-free-search.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-free-search.mjs
|
||||
COPY install-workspace.mjs /usr/local/lib/dsh-mgr/install-workspace.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-workspace.mjs
|
||||
COPY init-profile.mjs /usr/local/lib/dsh-mgr/init-profile.mjs
|
||||
COPY --from=proxy /usr/bin/caddy /usr/bin/caddy
|
||||
# Strip upstream file capabilities without granting any runtime capabilities.
|
||||
# A fresh file copy omits security.capability xattrs; port 8080 needs none.
|
||||
RUN cp /usr/bin/caddy /usr/bin/caddy.clean && mv /usr/bin/caddy.clean /usr/bin/caddy
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY entrypoint.sh /usr/local/bin/dsh-entrypoint
|
||||
RUN chmod 755 /usr/local/bin/dsh-entrypoint && mkdir -p /data /workspace /shared && ln -s /workspace /data/dsh-home && chown -R node:node /data /workspace /shared
|
||||
ARG RECIPE_REV=7
|
||||
LABEL dsh-mgr.role=runtime dsh-mgr.version=$DSH_VERSION dsh-mgr.recipe=$RECIPE_REV
|
||||
ENV DSH_HOME=/workspace HOME=/workspace XDG_DATA_HOME=/workspace/.local/share XDG_CONFIG_HOME=/workspace/.config
|
||||
USER node
|
||||
WORKDIR /workspace
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "/usr/local/bin/dsh-entrypoint"]
|
||||
CMD ["web", "--no-open", "--port", "3080"]
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export npm_config_ignore_scripts=true npm_config_ignore_pnpmfile=true
|
||||
node /usr/local/lib/dsh-mgr/init-profile.mjs
|
||||
if [[ -f "${DSH_HOME}/profiles/web/package.json" ]]; then
|
||||
echo "dsh-mgr: plugin prestart reconciliation starting" >&2
|
||||
# pnpm otherwise probes registry metadata for the overlapping optional peer.
|
||||
# Restrict offline mode to profiles containing only our fixed archives.
|
||||
install_env=()
|
||||
if node -e 'const m=require(process.env.DSH_HOME+"/profiles/web/package.json"); const pins={"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.0.5.tgz","dsh-free-search":"file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz"}; process.exit(Object.entries({...m.dependencies,...m.devDependencies,...m.optionalDependencies}).every(([n,v])=>pins[n]===v)?0:1)'; then
|
||||
install_env=(npm_config_offline=true)
|
||||
fi
|
||||
if timeout --signal=TERM --kill-after=5s 120s env CI=true "${install_env[@]}" dsh plugin --profile web install --ignore-scripts; then
|
||||
echo "dsh-mgr: plugin prestart reconciliation complete" >&2
|
||||
else
|
||||
status=$?
|
||||
echo "dsh-mgr: plugin prestart reconciliation failed (exit $status); application not started" >&2
|
||||
exit "$status"
|
||||
fi
|
||||
else
|
||||
echo "dsh-mgr: no web plugin manifest; using bundled plugins" >&2
|
||||
fi
|
||||
|
||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile &
|
||||
proxy_pid=$!
|
||||
echo
|
||||
dsh "$@" &
|
||||
dsh_pid=$!
|
||||
cleanup() {
|
||||
trap - TERM INT
|
||||
kill -TERM "$proxy_pid" "$dsh_pid" 2>/dev/null || true
|
||||
wait "$proxy_pid" 2>/dev/null || true
|
||||
wait "$dsh_pid" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
# Either child exiting terminates the container; never leave a proxy-only instance.
|
||||
set +e
|
||||
wait -n "$proxy_pid" "$dsh_pid"
|
||||
status=$?
|
||||
set -e
|
||||
exit "$status"
|
||||
@@ -0,0 +1,76 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
for (const status of ['0', '23', '124', '137', 'empty']) {
|
||||
test('runtime prestart status ' + status, () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-prestart-'));
|
||||
try {
|
||||
if (status !== 'empty') {
|
||||
mkdirSync(join(dir, 'profiles/web'), { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(dir, 'profiles/web/package.json'), '{}', { mode: 0o600 });
|
||||
}
|
||||
const scripts = {
|
||||
node: `#!/bin/bash
|
||||
exit 0
|
||||
`,
|
||||
timeout: `#!/bin/bash
|
||||
printf 'timeout %s\n' "$*" >> "$TRACE"
|
||||
test "$1 $2 $3" = "--signal=TERM --kill-after=5s 120s" || exit 90
|
||||
shift 3
|
||||
exec "$@"
|
||||
`,
|
||||
dsh: `#!/bin/bash
|
||||
printf 'dsh %s\n' "$*" >> "$TRACE"
|
||||
test "$npm_config_ignore_scripts:$npm_config_ignore_pnpmfile" = true:true || exit 91
|
||||
if [ "$1" = plugin ]; then
|
||||
test "$*" = "plugin --profile web install --ignore-scripts" || exit 92
|
||||
exit "$PLUGIN_STATUS"
|
||||
fi
|
||||
exit 0
|
||||
`,
|
||||
caddy: `#!/bin/bash
|
||||
echo caddy >> "$TRACE"
|
||||
sleep 0.1
|
||||
`,
|
||||
};
|
||||
for (const [name, script] of Object.entries(scripts)) {
|
||||
writeFileSync(join(dir, name), script, { mode: 0o700 });
|
||||
}
|
||||
const result = spawnSync('bash', [fileURLToPath(new URL('./entrypoint.sh', import.meta.url)), 'web', '--no-open', '--port', '3080'], {
|
||||
env: { ...process.env, PATH: dir + ':' + process.env.PATH, DSH_HOME: dir, TRACE: join(dir, 'trace'), PLUGIN_STATUS: status },
|
||||
encoding: 'utf8', timeout: 5000,
|
||||
});
|
||||
assert.ifError(result.error);
|
||||
const output = result.stdout + result.stderr;
|
||||
const trace = readFileSync(join(dir, 'trace'), 'utf8');
|
||||
assert.equal(result.status, status === 'empty' ? 0 : Number(status), output);
|
||||
if (status === 'empty') {
|
||||
assert.doesNotMatch(trace, /plugin/);
|
||||
assert.match(trace, /dsh web/);
|
||||
return;
|
||||
}
|
||||
assert.match(trace, /dsh plugin --profile web install --ignore-scripts/);
|
||||
if (status === '0') {
|
||||
assert.match(output, /reconciliation complete/);
|
||||
assert.match(trace, /dsh web/);
|
||||
} else {
|
||||
assert.ok(output.includes('failed (exit ' + status + ')'), output);
|
||||
assert.doesNotMatch(trace, /caddy|dsh web/);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('runtime pins pnpm and uses nonroot with tini process-group forwarding', () => {
|
||||
const dockerfile = readFileSync(new URL('./Dockerfile', import.meta.url), 'utf8');
|
||||
for (const required of ['[email protected]', 'test "$(pnpm --version)" = "10.11.0"', 'USER node', '"/usr/bin/tini", "-g"']) {
|
||||
assert.ok(dockerfile.includes(required), 'missing ' + required);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { mkdirSync, existsSync, writeFileSync, readFileSync, readdirSync, lstatSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const home = process.env.DSH_HOME;
|
||||
if (!home) throw new Error('DSH_HOME required');
|
||||
const profile = join(home, 'profiles/web');
|
||||
for (const dir of [home, join(home, 'profiles'), profile]) {
|
||||
try {
|
||||
const stat = lstatSync(dir);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('unsafe profile directory');
|
||||
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
||||
}
|
||||
// Never rewrite an existing user manifest or patch.
|
||||
if (!existsSync(profile) || readdirSync(profile).length === 0) {
|
||||
mkdirSync(profile, { recursive: true });
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web', private: true,
|
||||
dependencies: { '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.0.5.tgz', 'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz' },
|
||||
pnpm: { overrides: {
|
||||
'dsh-free-search>@deepseek-ai/schemastery': 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz',
|
||||
'@deepseek-ai/schemastery>@deepseek-ai/cosmokit': 'file:/usr/local/lib/dsh-mgr/cosmokit-1.8.5.tgz',
|
||||
'@deepseek-ai/schemastery>@standard-schema/spec': 'file:/usr/local/lib/dsh-mgr/spec-1.1.0.tgz',
|
||||
} },
|
||||
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dsh-web-mobile', 'dsh-free-search', 'dsh-mgr-workspace'] } },
|
||||
}, null, 2), { flag: 'wx', mode: 0o600 });
|
||||
console.log('dsh-mgr: new web profile includes [email protected] and [email protected]');
|
||||
}
|
||||
const manifestPath = join(profile, 'package.json');
|
||||
const workspacePath = join(profile, 'pnpm-workspace.yaml');
|
||||
// Backups omit workspace settings; restore host-provided peers before pnpm runs.
|
||||
if (existsSync(manifestPath) && !existsSync(workspacePath)) {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
if (manifest.dependencies?.['dsh-web-mobile'] === 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz' || manifest.dependencies?.['dsh-free-search'] === 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz') {
|
||||
writeFileSync(workspacePath, 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n', { flag: 'wx', mode: 0o600 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, symlinkSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
test('new profile enables bundle once and preserves existing configuration', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-init-'));
|
||||
try {
|
||||
const run = () => execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], {env:{...process.env, DSH_HOME:home}});
|
||||
run();
|
||||
const file=join(home,'profiles/web/package.json');
|
||||
const original=readFileSync(file,'utf8');
|
||||
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-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-free-search'], 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.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.0.5.tgz');
|
||||
const workspace = join(home, 'profiles/web/pnpm-workspace.yaml');
|
||||
assert.match(readFileSync(workspace, 'utf8'), /autoInstallPeers: false/);
|
||||
assert.match(readFileSync(workspace, 'utf8'), /nodeLinker: hoisted/);
|
||||
run();assert.equal(readFileSync(file,'utf8'),original);
|
||||
const legacy = JSON.parse(original);
|
||||
delete legacy.dependencies['dsh-mgr-workspace'];
|
||||
legacy.dsh.profile.bundles = legacy.dsh.profile.bundles.filter(name => name !== 'dsh-mgr-workspace');
|
||||
writeFileSync(file, JSON.stringify(legacy));
|
||||
run();
|
||||
assert.deepEqual(JSON.parse(readFileSync(file)), legacy);
|
||||
const disabled = JSON.parse(original);
|
||||
disabled.dsh.profile.bundles = disabled.dsh.profile.bundles.filter(name => name !== 'dsh-web-mobile');
|
||||
writeFileSync(file, JSON.stringify(disabled));
|
||||
writeFileSync(workspace, 'custom workspace');
|
||||
const patch = join(home, 'profiles/web/cordis.patch.yml');
|
||||
writeFileSync(patch, '[]');
|
||||
run();
|
||||
assert.deepEqual(JSON.parse(readFileSync(file)), disabled);
|
||||
assert.equal(readFileSync(workspace, 'utf8'), 'custom workspace');
|
||||
assert.equal(readFileSync(patch, 'utf8'), '[]');
|
||||
const custom=JSON.stringify({custom:true});
|
||||
writeFileSync(file,custom);run();assert.equal(readFileSync(file,'utf8'),custom);
|
||||
} finally { rmSync(home,{recursive:true,force:true}); }
|
||||
});
|
||||
test('nonempty legacy profiles are not automatically injected', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-legacy-'));
|
||||
try {
|
||||
const profile = join(home, 'profiles/web');
|
||||
mkdirSync(profile, { recursive: true });
|
||||
writeFileSync(join(profile, 'cordis.patch.yml'), '[]');
|
||||
execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], { env: { ...process.env, DSH_HOME: home } });
|
||||
assert.equal(existsSync(join(profile, 'package.json')), false);
|
||||
assert.equal(existsSync(join(profile, 'pnpm-workspace.yaml')), false);
|
||||
} finally { rmSync(home, { recursive: true, force: true }); }
|
||||
});
|
||||
test('rejects symlinked profile ancestors', () => {
|
||||
const home=mkdtempSync(join(tmpdir(),'dsh-plugin-init-'));
|
||||
const outside=mkdtempSync(join(tmpdir(),'dsh-plugin-outside-'));
|
||||
try {
|
||||
symlinkSync(outside,join(home,'profiles'));
|
||||
assert.throws(()=>execFileSync(process.execPath,[new URL('./init-profile.mjs',import.meta.url).pathname],{env:{...process.env,DSH_HOME:home},stdio:'pipe'}));
|
||||
assert.equal(existsSync(join(outside,'web/package.json')),false);
|
||||
} finally {rmSync(home,{recursive:true,force:true});rmSync(outside,{recursive:true,force:true});}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
const root = '/usr/local/lib/dsh-mgr';
|
||||
const packages = [
|
||||
['dsh-free-search', '0.8.5', 'https://registry.npmjs.org/dsh-free-search/-/dsh-free-search-0.8.5.tgz', 'MT00Xtypw9uk9QsqaiRmtVbYwnPON+HJY1lZjYritEvuwSe9ahgG9NVO43lUI+Eje2umrqUapaeqLRYf34DrZw=='],
|
||||
['@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=='],
|
||||
['@standard-schema/spec', '1.1.0', 'https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz', 'l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=='],
|
||||
];
|
||||
for (const [name, version, url, integrity] of packages) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
|
||||
if (!response.ok) throw new Error('download failed: ' + name);
|
||||
const data = Buffer.from(await response.arrayBuffer());
|
||||
if (createHash('sha512').update(data).digest('base64') !== integrity) throw new Error('integrity mismatch: ' + name);
|
||||
const archive = `${root}/${name.split('/').pop()}-${version}.tgz`;
|
||||
writeFileSync(archive, data, { mode: 0o644 });
|
||||
}
|
||||
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';
|
||||
mkdirSync(destination, { recursive: true });
|
||||
execFileSync('tar', ['-xzf', `${root}/dsh-free-search-0.8.5.tgz`, '--strip-components=1', '--no-same-owner', '-C', destination]);
|
||||
execFileSync('chmod', ['-R', 'a+rX', destination]);
|
||||
const pkg = JSON.parse(readFileSync(destination + '/package.json'));
|
||||
if (pkg.name !== 'dsh-free-search' || pkg.version !== '0.8.5' || JSON.stringify(pkg.dependencies) !== JSON.stringify({ '@deepseek-ai/schemastery': '^3.18.4' })) throw new Error('unexpected free-search metadata');
|
||||
const require = createRequire(hostManifest);
|
||||
const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver');
|
||||
for (const [name, range] of Object.entries({ ...pkg.peerDependencies, ...pkg.dependencies })) {
|
||||
if (name === 'react') {
|
||||
if (!['0.2.0-rc.1', '0.2.0-rc.2'].includes(require('./package.json').version)) throw new Error('unverified browser React host');
|
||||
continue;
|
||||
}
|
||||
const peer = require(name + '/package.json');
|
||||
if (!semver.satisfies(peer.version, range)) throw new Error('incompatible free-search host dependency: ' + name);
|
||||
}
|
||||
const host = JSON.parse(readFileSync(hostManifest));
|
||||
host.dependencies = { ...host.dependencies, 'dsh-free-search': '0.8.5' };
|
||||
writeFileSync(hostManifest, JSON.stringify(host, null, 2));
|
||||
console.log('Installed [email protected] and pinned offline dependency archives (SHA-512 verified; no lifecycle scripts)');
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
const version = '3.0.5';
|
||||
const integrity = 'N3wmegFHSd23J0+xjotKyBq0ZbLXUbkWZ65E+aSw5pzB/nudhaNHWT2V3CSNaW/LX3s6AxAWB2APpfA0dLXJZQ==';
|
||||
const response = await fetch('https://registry.npmjs.org/dsh-web-mobile/-/dsh-web-mobile-3.0.5.tgz', { signal: AbortSignal.timeout(120000) });
|
||||
if (!response.ok) throw new Error('mobile plugin download failed');
|
||||
const data = Buffer.from(await response.arrayBuffer());
|
||||
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';
|
||||
mkdirSync(destination, { recursive: true });
|
||||
const archive = '/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz';
|
||||
writeFileSync(archive, data, { mode: 0o644 });
|
||||
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.
|
||||
execFileSync('chmod', ['-R', 'a+rX', destination]);
|
||||
const pkg = JSON.parse(readFileSync(destination + '/package.json'));
|
||||
if (pkg.version !== version || pkg.name !== 'dsh-web-mobile' || Object.keys(pkg.dependencies || {}).length) throw new Error('unexpected mobile package metadata');
|
||||
const require = createRequire('/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json');
|
||||
const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver');
|
||||
for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
|
||||
// React is provided by the browser shell, not a Node package in DSH.
|
||||
// Limit that exception to the upstream-declared, tested RC hosts.
|
||||
if (name === 'react') {
|
||||
const host = require('./package.json').version;
|
||||
if (!['0.2.0-rc.1', '0.2.0-rc.2'].includes(host)) throw new Error('unverified browser React host: ' + host);
|
||||
continue;
|
||||
}
|
||||
let peer;
|
||||
try { peer = require(name + '/package.json'); }
|
||||
catch (error) {
|
||||
if (pkg.peerDependenciesMeta?.[name]?.optional) continue;
|
||||
throw new Error('missing required mobile peer: ' + name);
|
||||
}
|
||||
if (!semver.satisfies(peer.version, range)) throw new Error('incompatible mobile peer: ' + name + '@' + peer.version);
|
||||
}
|
||||
// DSH runtime resolution traverses the installation manifest, not arbitrary
|
||||
// directories in node_modules. Declare the baked package in that graph.
|
||||
const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json';
|
||||
const host = JSON.parse(readFileSync(hostManifest));
|
||||
host.dependencies = { ...host.dependencies, 'dsh-web-mobile': version };
|
||||
writeFileSync(hostManifest, JSON.stringify(host, null, 2));
|
||||
console.log('Installed [email protected] (SHA-512 verified; no lifecycle scripts)');
|
||||
@@ -0,0 +1,12 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
const root = '/usr/local/lib/dsh-mgr';
|
||||
const destination = root + '/workspace-package/package';
|
||||
mkdirSync(destination, { recursive: true });
|
||||
writeFileSync(destination + '/package.json', JSON.stringify({
|
||||
name: 'dsh-mgr-workspace', version: '1.0.0', type: 'module', main: 'index.js',
|
||||
dsh: { bundle: { patch: 'cordis.patch.yml' } },
|
||||
}));
|
||||
writeFileSync(destination + '/index.js', "export const inject = ['workspaceRegistry', 'sessions'];\nexport async function apply(ctx) {\n await ctx.workspaceRegistry.initializeDefault(() => '/workspace');\n}\n");
|
||||
writeFileSync(destination + '/cordis.patch.yml', "- insert:\n - id: dsh-mgr-workspace\n name: dsh-mgr-workspace\n");
|
||||
execFileSync('tar', ['-czf', root + '/dsh-mgr-workspace-1.0.0.tgz', '-C', root + '/workspace-package', 'package']);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"release_id": "runtime-7.1",
|
||||
"version": "0.2.0-rc.2",
|
||||
"deployment_schema": 7,
|
||||
"platforms": ["linux/amd64"]
|
||||
}
|
||||
Reference in New Issue
Block a user